Compare commits

...

177 Commits
3.5 ... 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
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
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
hikaruworld
f52bd2bcc0 support Desktop in Clone 2014-08-12 21:45:03 +09:00
111 changed files with 2440 additions and 1599 deletions

View File

@@ -1,5 +1,6 @@
language: scala
sudo: false
script:
- . env.sh
- 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.

View File

@@ -1,4 +1,4 @@
GitBucket [![Gitter chat](https://badges.gitter.im/takezoe/gitbucket.png)](https://gitter.im/takezoe/gitbucket) [![Build Status](https://travis-ci.org/takezoe/gitbucket.svg?branch=master)](https://travis-ci.org/takezoe/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 powered by Scala.
@@ -14,29 +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!
- 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**.
@@ -49,7 +42,7 @@ 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
@@ -72,20 +65,59 @@ Or, if you don't want/need launchctl, you can just run:
```
#### 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/takezoe/gitbucket-gist-plugin)
- 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

View File

@@ -44,7 +44,7 @@ GITBUCKET_WAR_DIR=$GITBUCKET_DIR/lib
GITBUCKET_WAR_FILE=$GITBUCKET_WAR_DIR/gitbucket.war
# GitBucket version to fetch when installing
GITBUCKET_VERSION=2.1
GITBUCKET_VERSION=3.5
#
# End of configuration section. Ignore this part

View File

@@ -38,7 +38,7 @@ 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/takezoe/gitbucket/releases/download/$GITBUCKET_VERSION/gitbucket.war
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"

View File

@@ -3,7 +3,7 @@ Summary: GitHub clone written with Scala.
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

View File

@@ -2,7 +2,7 @@ Automatic Schema Updating
========
GitBucket uses H2 database to manage project and account data. GitBucket updates database schema automatically in the first run after the upgrading.
To release a new version of GitBucket, add the version definition to the [servlet.AutoUpdate](https://github.com/takezoe/gitbucket/blob/master/src/main/scala/servlet/AutoUpdateListener.scala) at first.
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 {
@@ -16,7 +16,7 @@ object AutoUpdate {
...
```
Next, add a SQL file which updates database schema into [/src/main/resources/update/](https://github.com/takezoe/gitbucket/tree/master/src/main/resources/update) as ```MAJOR_MINOR.sql```.
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.

3
env.sh
View File

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

View File

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

View File

@@ -10,7 +10,7 @@ import sbtassembly.AssemblyKeys._
object MyBuild extends Build {
val Organization = "gitbucket"
val Name = "gitbucket"
val Version = "3.5.0"
val Version = "3.8.0"
val ScalaVersion = "2.11.6"
val ScalatraVersion = "2.3.1"
@@ -38,7 +38,8 @@ object MyBuild extends Build {
scalaVersion := ScalaVersion,
resolvers ++= Seq(
Classpaths.typesafeReleases,
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
"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(
@@ -50,11 +51,12 @@ object MyBuild extends Build {
"org.json4s" %% "json4s-jackson" % "3.2.11",
"jp.sf.amateras" %% "scalatra-forms" % "0.1.0",
"commons-io" % "commons-io" % "2.4",
"org.pegdown" % "pegdown" % "1.5.0",
"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",

View File

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

View File

@@ -1,5 +1,5 @@
#!/bin/sh
. ../env.sh
. ./env.sh
cd ../
./sbt.sh clean assembly

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"

View File

@@ -3,13 +3,13 @@ D="$(dirname "$0")"
D="$(cd "${D}"; pwd)"
DD="$(dirname "${D}")"
(
for f in "${DD}/env.sh" "${D}/build.xml"; do
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}"
. "${DD}/env.sh"
ant -f "${D}/build.xml" all
)

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

2
sbt.sh
View File

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

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

@@ -32,6 +32,7 @@ class ScalatraBootstrap extends LifeCycle {
context.mount(new DashboardController, "/*")
context.mount(new UserManagementController, "/*")
context.mount(new SystemSettingsController, "/*")
context.mount(new PluginsController, "/*")
context.mount(new AccountController, "/*")
context.mount(new RepositoryViewerController, "/*")
context.mount(new WikiController, "/*")

View File

@@ -14,16 +14,16 @@ case class ApiComment(
user: ApiUser,
body: String,
created_at: Date,
updated_at: Date)(repositoryName: RepositoryName, issueId: Int){
val html_url = ApiPath(s"/${repositoryName.fullName}/issues/${issueId}#comment-${id}")
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): 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)
updated_at = comment.updatedDate)(repositoryName, issueId, isPullRequest)
}

View File

@@ -20,13 +20,21 @@ case class ApiCommit(
removed: List[String],
modified: List[String],
author: ApiPersonIdent,
committer: ApiPersonIdent)(repositoryName:RepositoryName){
val url = ApiPath(s"/api/v3/${repositoryName.fullName}/commits/${id}")
val html_url = ApiPath(s"/${repositoryName.fullName}/commit/${id}")
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): 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,
@@ -43,6 +51,7 @@ object ApiCommit{
},
author = ApiPersonIdent.author(commit),
committer = ApiPersonIdent.committer(commit)
)(repositoryName)
)(repositoryName, urlIsHtmlUrl)
}
def forPushPayload(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiCommit = apply(git, repositoryName, commit, true)
}

View File

@@ -17,9 +17,9 @@ case class ApiIssue(
state: String,
created_at: Date,
updated_at: Date,
body: String)(repositoryName: RepositoryName){
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}/issues/${number}")
val html_url = ApiPath(s"/${repositoryName.fullName}/${if(isPullRequest){ "pull" }else{ "issues" }}/${number}")
}
object ApiIssue{
@@ -31,5 +31,5 @@ object ApiIssue{
state = if(issue.closed){ "closed" }else{ "open" },
body = issue.content.getOrElse(""),
created_at = issue.registeredDate,
updated_at = issue.updatedDate)(repositoryName)
updated_at = issue.updatedDate)(repositoryName, issue.isPullRequest)
}

View File

@@ -13,10 +13,14 @@ case class ApiRepository(
forks: Int,
`private`: Boolean,
default_branch: String,
owner: ApiUser) {
owner: ApiUser)(urlIsHtmlUrl: Boolean) {
val forks_count = forks
val watchers_coun = watchers
val url = ApiPath(s"/api/v3/repos/${full_name}")
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}")
@@ -27,7 +31,8 @@ object ApiRepository{
repository: Repository,
owner: ApiUser,
forkedCount: Int =0,
watchers: Int = 0): ApiRepository =
watchers: Int = 0,
urlIsHtmlUrl: Boolean = false): ApiRepository =
ApiRepository(
name = repository.repositoryName,
full_name = s"${repository.userName}/${repository.repositoryName}",
@@ -37,7 +42,7 @@ object ApiRepository{
`private` = repository.isPrivate,
default_branch = repository.defaultBranch,
owner = owner
)
)(urlIsHtmlUrl)
def apply(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount)
@@ -45,4 +50,7 @@ object ApiRepository{
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,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,4 @@
package gitbucket.core.api
/** export fields for json */
trait FieldSerializable

View File

@@ -22,7 +22,7 @@ object JsonFormat {
)
) + FieldSerializer[ApiUser]() + FieldSerializer[ApiPullRequest]() + FieldSerializer[ApiRepository]() +
FieldSerializer[ApiCommitListItem.Parent]() + FieldSerializer[ApiCommitListItem]() + FieldSerializer[ApiCommitListItem.Commit]() +
FieldSerializer[ApiCommitStatus]() + FieldSerializer[ApiCommit]() + FieldSerializer[ApiCombinedCommitStatus]() +
FieldSerializer[ApiCommitStatus]() + FieldSerializer[FieldSerializable]() + FieldSerializer[ApiCombinedCommitStatus]() +
FieldSerializer[ApiPullRequest.Commit]() + FieldSerializer[ApiIssue]() + FieldSerializer[ApiComment]()

View File

@@ -366,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),
Constants.HEAD, 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
@@ -423,6 +374,54 @@ 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
@@ -496,6 +495,59 @@ trait AccountControllerBase extends AccountManagementControllerBase {
}
})
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")

View File

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

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

@@ -86,7 +86,7 @@ trait IssuesControllerBase extends ControllerBase {
issueId <- params("id").toIntOpt
comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt)
} yield {
JsonFormat(comments.map{ case (issueComment, user) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user)) })
JsonFormat(comments.map{ case (issueComment, user, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) })
}).getOrElse(NotFound)
})
@@ -190,7 +190,7 @@ trait IssuesControllerBase extends ControllerBase {
(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)))
JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get), issue.isPullRequest))
}) getOrElse NotFound
})
@@ -233,7 +233,7 @@ trait IssuesControllerBase extends ControllerBase {
org.json4s.jackson.Serialization.write(
Map("title" -> x.title,
"content" -> Markdown.toHtml(x.content getOrElse "No description given.",
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.openedUserName))
repository, false, true, true, true, isEditable(x.userName, x.repositoryName, x.openedUserName))
))
}
} else Unauthorized
@@ -257,6 +257,12 @@ trait IssuesControllerBase extends ControllerBase {
} 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)
@@ -326,6 +332,7 @@ 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 =>
response.setHeader("Content-Disposition", f"""inline; filename=${file.getName}""")
RawData(FileUtil.getMimeType(file.getName), file)
}
case _ => None
@@ -346,6 +353,7 @@ trait IssuesControllerBase extends ControllerBase {
}
}
// 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
@@ -459,7 +467,11 @@ trait IssuesControllerBase extends ControllerBase {
"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" ), false, owner -> repoName),

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

@@ -46,7 +46,10 @@ trait PullRequestsControllerBase extends ControllerBase {
"requestRepositoryName" -> trim(text(required, maxlength(100))),
"requestBranch" -> trim(text(required, maxlength(100))),
"commitIdFrom" -> trim(text(required, maxlength(40))),
"commitIdTo" -> 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(
@@ -62,7 +65,11 @@ trait PullRequestsControllerBase extends ControllerBase {
requestRepositoryName: String,
requestBranch: String,
commitIdFrom: String,
commitIdTo: String)
commitIdTo: String,
assignedUserName: Option[String],
milestoneId: Option[Int],
labelNames: Option[String]
)
case class MergeForm(message: String)
@@ -176,7 +183,7 @@ trait PullRequestsControllerBase extends ControllerBase {
pullreq,
statuses,
repository,
s"${context.baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName, context.baseUrl).get)
}
} getOrElse NotFound
})
@@ -232,6 +239,9 @@ trait PullRequestsControllerBase extends ControllerBase {
}
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
}
updatePullRequests(owner, name, pullreq.branch)
// call web hook
callPullRequestWebHook("closed", repository, issueId, context.baseUrl, context.loginAccount.get)
@@ -284,6 +294,9 @@ trait PullRequestsControllerBase extends ControllerBase {
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
@@ -307,32 +320,44 @@ trait PullRequestsControllerBase extends ControllerBase {
originRepository.owner, originRepository.name, originId,
forkedRepository.owner, forkedRepository.name, forkedId)
(oldGit.getRepository.resolve(rootId), newGit.getRepository.resolve(forkedId))
(Option(oldGit.getRepository.resolve(rootId)), Option(newGit.getRepository.resolve(forkedId)))
} else {
// Commit id
(oldGit.getRepository.resolve(originId), newGit.getRepository.resolve(forkedId))
(Option(oldGit.getRepository.resolve(originId)), Option(newGit.getRepository.resolve(forkedId)))
}
val (commits, diffs) = getRequestCompareInfo(
originRepository.owner, originRepository.name, oldId.getName,
forkedRepository.owner, forkedRepository.name, newId.getName)
(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(forkedRepository.owner, forkedRepository.name, context.loginAccount))
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
})
@@ -368,47 +393,78 @@ trait PullRequestsControllerBase extends ControllerBase {
})
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
val loginUserName = context.loginAccount.get.userName
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 = None,
milestoneId = None,
isPullRequest = true)
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)
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
fetchAsPullRequest(repository.owner, repository.name, form.requestUserName, form.requestRepositoryName, form.requestBranch, issueId)
// 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)
}
}
}
}
// record activity
recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title)
// fetch requested branch
fetchAsPullRequest(owner, name, form.requestUserName, form.requestRepositoryName, form.requestBranch, issueId)
// call web hook
callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get)
// record activity
recordPullRequestActivity(owner, name, loginUserName, issueId, form.title)
// notifications
getIssue(repository.owner, repository.name, issueId.toString) foreach { issue =>
Notifier().toNotify(repository, issue, form.content.getOrElse("")){
Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
// 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")
}
}
}
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
})
}
/**
* Parses branch identifier and extracts owner and branch name as tuple.
@@ -459,7 +515,11 @@ trait PullRequestsControllerBase extends ControllerBase {
"pulls",
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, 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" ), true, owner -> repoName),

View File

@@ -14,6 +14,7 @@ import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Constants
import org.eclipse.jgit.lib.ObjectId
class RepositorySettingsController extends RepositorySettingsControllerBase
@@ -165,13 +166,15 @@ trait RepositorySettingsControllerBase extends ControllerBase {
import scala.collection.JavaConverters._
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
getAccountByUserName(repository.owner).foreach { ownerAccount =>
callWebHook("push",
List(WebHook(repository.owner, repository.name, form.url)),
WebHookPushPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)
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

View File

@@ -22,6 +22,7 @@ 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._
@@ -249,7 +250,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
)
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}"
if(form.path.length == 0) urlEncode(form.newFileName) else s"${form.path}/${urlEncode(form.newFileName)}"
}")
})
@@ -270,7 +271,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
)
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}"
if(form.path.length == 0) urlEncode(form.newFileName) else s"${form.path}/${urlEncode(form.newFileName)}"
}")
})
@@ -292,8 +293,12 @@ trait RepositoryViewerControllerBase extends ControllerBase {
getPathObjectId(git, path, revCommit).map { objectId =>
if(raw){
// Download
JGitUtil.getContentFromId(git, objectId, true).map { bytes =>
RawData("application/octet-stream", bytes)
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,
@@ -344,16 +349,21 @@ trait RepositoryViewerControllerBase extends ControllerBase {
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) =>
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))
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
}
})
@@ -517,10 +527,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
/**
* Displays the file find of branch.
*/
get("/:owner/:repository/find/:ref")(referrersOnly { repository =>
get("/:owner/:repository/find/*")(referrersOnly { repository =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getTreeId(git, params("ref")).map{ treeId =>
html.find(params("ref"),
val ref = multiParams("splat").head
JGitUtil.getTreeId(git, ref).map{ treeId =>
html.find(ref,
treeId,
repository,
context.loginAccount match {
@@ -658,7 +669,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
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)
WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount,
oldId = headTip, newId = commitId)
}
}
}

View File

@@ -24,7 +24,8 @@ trait SystemSettingsControllerBase extends ControllerBase {
"activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))),
"ssh" -> trim(label("SSH access", boolean())),
"sshPort" -> trim(label("SSH port", optional(number()))),
"smtp" -> optionalIfNotChecked("notification", mapping(
"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()))),

View File

@@ -22,11 +22,13 @@ trait IssuesService {
def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) =
IssueComments filter (_.byIssue(owner, repository, issueId)) list
/** @return IssueComment and commentedUser */
def getCommentsForApi(owner: String, repository: String, issueId: Int)(implicit s: Session): List[(IssueComment, Account)] =
/** @return IssueComment and commentedUser and Issue */
def getCommentsForApi(owner: String, repository: String, issueId: Int)(implicit s: Session): List[(IssueComment, Account, Issue)] =
IssueComments.filter(_.byIssue(owner, repository, issueId))
.filter(_.action inSetBind Set("comment" , "close_comment", "reopen_comment"))
.innerJoin(Accounts).on( (t1, t2) => t1.commentedUserName === t2.userName )
.innerJoin(Issues).on{ case ((t1, t2), t3) => t3.byIssue(t1.userName, t1.repositoryName, t1.issueId) }
.map{ case ((t1, t2), t3) => (t1, t2, t3) }
.list
def getComment(owner: String, repository: String, commentId: String)(implicit s: Session) =
@@ -90,7 +92,7 @@ trait IssuesService {
def getCommitStatues(issueList:Seq[(String, String, Int)])(implicit s: Session) :Map[(String, String, Int), CommitStatusInfo] ={
if(issueList.isEmpty){
Map.empty
}else{
} else {
import scala.slick.jdbc._
val issueIdQuery = issueList.map(i => "(PR.USER_NAME=? AND PR.REPOSITORY_NAME=? AND PR.ISSUE_ID=?)").mkString(" OR ")
implicit val qset = SetParameter[Seq[(String, String, Int)]] {
@@ -474,9 +476,11 @@ object IssuesService {
* Restores IssueSearchCondition instance from filter query.
*/
def apply(filter: String, milestones: Map[String, Int]): IssueSearchCondition = {
val conditions = filter.split("[  \t]+").map { x =>
val dim = x.split(":")
dim(0) -> dim(1)
val conditions = filter.split("[  \t]+").flatMap { x =>
x.split(":") match {
case Array(key, value) => Some((key, value))
case _ => None
}
}.groupBy(_._1).map { case (key, values) =>
key -> values.map(_._2).toSeq
}

View File

@@ -66,10 +66,6 @@ trait RepositoryService { self: AccountService =>
(t.parentUserName === oldUserName.bind) && (t.parentRepositoryName === oldRepositoryName.bind)
}.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName)
PullRequests.filter { t =>
t.requestRepositoryName === oldRepositoryName.bind
}.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName)
// Updates activity fk before deleting repository because activity is sorted by activityId
// and it can't be changed by deleting-and-inserting record.
Activities.filter(_.byRepository(oldUserName, oldRepositoryName)).list.foreach { activity =>
@@ -98,6 +94,11 @@ trait RepositoryService { self: AccountService =>
CommitComments.insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
CommitStatuses.insertAll(commitStatuses.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
// Update source repository of pull requests
PullRequests.filter { t =>
(t.requestUserName === oldUserName.bind) && (t.requestRepositoryName === oldRepositoryName.bind)
}.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName)
// Convert labelId
val oldLabelMap = labels.map(x => (x.labelId, x.labelName)).toMap
val newLabelMap = Labels.filter(_.byRepository(newUserName, newRepositoryName)).map(x => (x.labelName, x.labelId)).list.toMap
@@ -383,6 +384,12 @@ object RepositoryService {
def sshUrl(port: Int, userName: String) = s"ssh://${userName}@${host}:${port}/${owner}/${name}.git"
def sshOpenRepoUrl(platform: String, port: Int, userName: String) = openRepoUrl(platform, sshUrl(port, userName))
def httpOpenRepoUrl(platform: String) = openRepoUrl(platform, httpUrl)
def openRepoUrl(platform: String, openUrl: String) = s"github-${platform}://openRepo/${openUrl}"
/**
* Creates instance with issue count and pull request count.
*/

View File

@@ -22,7 +22,8 @@ trait SystemSettingsService {
settings.activityLogLimit.foreach(x => props.setProperty(ActivityLogLimit, x.toString))
props.setProperty(Ssh, settings.ssh.toString)
settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
if(settings.notification) {
props.setProperty(UseSMTP, settings.useSMTP.toString)
if(settings.useSMTP) {
settings.smtp.foreach { smtp =>
props.setProperty(SmtpHost, smtp.host)
smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString))
@@ -75,7 +76,8 @@ trait SystemSettingsService {
getOptionValue[Int](props, ActivityLogLimit, None),
getValue(props, Ssh, false),
getOptionValue(props, SshPort, Some(DefaultSshPort)),
if(getValue(props, Notification, false)){
getValue(props, UseSMTP, getValue(props, Notification, false)), // handle migration scenario from only notification to useSMTP
if(getValue(props, UseSMTP, getValue(props, Notification, false))){
Some(Smtp(
getValue(props, SmtpHost, ""),
getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
@@ -125,6 +127,7 @@ object SystemSettingsService {
activityLogLimit: Option[Int],
ssh: Boolean,
sshPort: Option[Int],
useSMTP: Boolean,
smtp: Option[Smtp],
ldapAuthentication: Boolean,
ldap: Option[Ldap]){
@@ -172,6 +175,7 @@ object SystemSettingsService {
private val ActivityLogLimit = "activity_log_limit"
private val Ssh = "ssh"
private val SshPort = "ssh.port"
private val UseSMTP = "useSMTP"
private val SmtpHost = "smtp.host"
private val SmtpPort = "smtp.port"
private val SmtpUser = "smtp.user"

View File

@@ -12,6 +12,7 @@ import org.apache.http.NameValuePair
import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.message.BasicNameValuePair
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.ObjectId
import org.slf4j.LoggerFactory
@@ -192,21 +193,33 @@ object WebHookService {
case class WebHookPushPayload(
pusher: ApiUser,
ref: String,
before: String,
after: String,
commits: List[ApiCommit],
repository: ApiRepository
) extends WebHookPayload
) extends FieldSerializable with WebHookPayload {
val compare = commits.size match {
case 0 => ApiPath(s"/${repository.full_name}") // maybe test hook on un-initalied repository
case 1 => ApiPath(s"/${repository.full_name}/commit/${after}")
case _ if before.filterNot(_=='0').isEmpty => ApiPath(s"/${repository.full_name}/compare/${commits.head.id}^...${after}")
case _ => ApiPath(s"/${repository.full_name}/compare/${before}...${after}")
}
val head_commit = commits.lastOption
}
object WebHookPushPayload {
def apply(git: Git, pusher: Account, refName: String, repositoryInfo: RepositoryInfo,
commits: List[CommitInfo], repositoryOwner: Account): WebHookPushPayload =
commits: List[CommitInfo], repositoryOwner: Account,
newId: ObjectId, oldId: ObjectId): WebHookPushPayload =
WebHookPushPayload(
ApiUser(pusher),
refName,
commits.map{ commit => ApiCommit(git, RepositoryName(repositoryInfo), commit) },
ApiRepository(
pusher = ApiUser(pusher),
ref = refName,
before = ObjectId.toString(oldId),
after = ObjectId.toString(newId),
commits = commits.map{ commit => ApiCommit.forPushPayload(git, RepositoryName(repositoryInfo), commit) },
repository = ApiRepository.forPushPayload(
repositoryInfo,
owner= ApiUser(repositoryOwner)
)
owner= ApiUser(repositoryOwner))
)
}
@@ -273,7 +286,7 @@ object WebHookService {
action = "created",
repository = ApiRepository(repository, repositoryUser),
issue = ApiIssue(issue, RepositoryName(repository), ApiUser(issueUser)),
comment = ApiComment(comment, RepositoryName(repository), issue.issueId, ApiUser(commentUser)),
comment = ApiComment(comment, RepositoryName(repository), issue.issueId, ApiUser(commentUser), issue.isPullRequest),
sender = ApiUser(sender))
}
}

View File

@@ -21,6 +21,17 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
new Version(3, 8),
new Version(3, 7) with SystemSettingsService {
override def update(conn: Connection, cl: ClassLoader): Unit = {
super.update(conn, cl)
val settings = loadSystemSettings()
if(settings.notification){
saveSystemSettings(settings.copy(useSMTP = true))
}
}
},
new Version(3, 6),
new Version(3, 5),
new Version(3, 4),
new Version(3, 3),

View File

@@ -203,7 +203,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
callWebHookOf(owner, repository, "push"){
for(pusherAccount <- getAccountByUserName(pusher);
ownerAccount <- getAccountByUserName(owner)) yield {
WebHookPushPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount)
WebHookPushPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount,
newId = command.getNewId(), oldId = command.getOldId())
}
}
}

View File

@@ -14,7 +14,7 @@ object SshServer {
private def configure(port: Int, baseUrl: String) = {
server.setPort(port)
server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(s"${Directory.GitBucketHome}/gitbucket.ser"))
server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(s"${Directory.GitBucketHome}/gitbucket.ser", "RSA"))
server.setPublickeyAuthenticator(new PublicKeyAuthenticator)
server.setCommandFactory(new GitCommandFactory(baseUrl))
server.setShellFactory(new NoShell)

View File

@@ -1,7 +1,7 @@
package gitbucket.core.util
import org.apache.commons.io.FileUtils
import java.net.URLConnection
import org.apache.tika.Tika
import java.io.File
import ControlUtil._
import scala.util.Random
@@ -9,8 +9,8 @@ import scala.util.Random
object FileUtil {
def getMimeType(name: String): String =
defining(URLConnection.getFileNameMap()){ fileNameMap =>
fileNameMap.getContentTypeFor(name) match {
defining(new Tika()){ tika =>
tika.detect(name) match {
case null => "application/octet-stream"
case mimeType => mimeType
}
@@ -28,6 +28,8 @@ object FileUtil {
def isImage(name: String): Boolean = getMimeType(name).startsWith("image/")
def isUploadableType(name: String): Boolean = mimeTypeWhiteList contains getMimeType(name)
def isLarge(size: Long): Boolean = (size > 1024 * 1000)
def isText(content: Array[Byte]): Boolean = !content.contains(0)
@@ -50,4 +52,14 @@ object FileUtil {
FileUtils.deleteDirectory(dir)
}
}
val mimeTypeWhiteList: Array[String] = Array(
"application/pdf",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"image/gif",
"image/jpeg",
"image/png",
"text/plain")
}

View File

@@ -65,6 +65,7 @@ object Implicits {
def paths: Array[String] = (request.getRequestURI.substring(request.getContextPath.length + 1) match{
case path if path.startsWith("api/v3/repos/") => path.substring(13/* "/api/v3/repos".length */)
case path if path.startsWith("api/v3/orgs/") => path.substring(12/* "/api/v3/orgs".length */)
case path => path
}).split("/")

View File

@@ -100,8 +100,18 @@ object JGitUtil {
def isDifferentFromAuthor: Boolean = authorName != committerName || authorEmailAddress != committerEmailAddress
}
case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String],
oldIsImage: Boolean, newIsImage: Boolean, oldObjectId: Option[String], newObjectId: Option[String])
case class DiffInfo(
changeType: ChangeType,
oldPath: String,
newPath: String,
oldContent: Option[String],
newContent: Option[String],
oldIsImage: Boolean,
newIsImage: Boolean,
oldObjectId: Option[String],
newObjectId: Option[String],
tooLarge: Boolean
)
/**
* The file content data for the file content view of the repository viewer.
@@ -495,11 +505,31 @@ object JGitUtil {
while(treeWalk.next){
val newIsImage = FileUtil.isImage(treeWalk.getPathString)
buffer.append((if(!fetchContent){
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None, false, newIsImage, None, Option(treeWalk.getObjectId(0)).map(_.name))
DiffInfo(
changeType = ChangeType.ADD,
oldPath = null,
newPath = treeWalk.getPathString,
oldContent = None,
newContent = None,
oldIsImage = false,
newIsImage = newIsImage,
oldObjectId = None,
newObjectId = Option(treeWalk.getObjectId(0)).map(_.name),
tooLarge = false
)
} else {
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None,
JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray),
false, newIsImage, None, Option(treeWalk.getObjectId(0)).map(_.name))
DiffInfo(
changeType = ChangeType.ADD,
oldPath = null,
newPath = treeWalk.getPathString,
oldContent = None,
newContent = JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray),
oldIsImage = false,
newIsImage = newIsImage,
oldObjectId = None,
newObjectId = Option(treeWalk.getObjectId(0)).map(_.name),
tooLarge = false
)
}))
}
(buffer.toList, None)
@@ -518,16 +548,52 @@ object JGitUtil {
import scala.collection.JavaConverters._
git.getRepository.getConfig.setString("diff", null, "renames", "copies")
git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff =>
val oldIsImage = FileUtil.isImage(diff.getOldPath)
val newIsImage = FileUtil.isImage(diff.getNewPath)
if(!fetchContent || oldIsImage || newIsImage){
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None, oldIsImage, newIsImage, Option(diff.getOldId).map(_.name), Option(diff.getNewId).map(_.name))
val diffs = git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala
diffs.map { diff =>
if(diffs.size > 100){
DiffInfo(
changeType = diff.getChangeType,
oldPath = diff.getOldPath,
newPath = diff.getNewPath,
oldContent = None,
newContent = None,
oldIsImage = false,
newIsImage = false,
oldObjectId = Option(diff.getOldId).map(_.name),
newObjectId = Option(diff.getNewId).map(_.name),
tooLarge = true
)
} else {
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
oldIsImage, newIsImage, Option(diff.getOldId).map(_.name), Option(diff.getNewId).map(_.name))
val oldIsImage = FileUtil.isImage(diff.getOldPath)
val newIsImage = FileUtil.isImage(diff.getNewPath)
if(!fetchContent || oldIsImage || newIsImage){
DiffInfo(
changeType = diff.getChangeType,
oldPath = diff.getOldPath,
newPath = diff.getNewPath,
oldContent = None,
newContent = None,
oldIsImage = oldIsImage,
newIsImage = newIsImage,
oldObjectId = Option(diff.getOldId).map(_.name),
newObjectId = Option(diff.getNewId).map(_.name),
tooLarge = false
)
} else {
DiffInfo(
changeType = diff.getChangeType,
oldPath = diff.getOldPath,
newPath = diff.getNewPath,
oldContent = JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
newContent = JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
oldIsImage = oldIsImage,
newIsImage = newIsImage,
oldObjectId = Option(diff.getOldId).map(_.name),
newObjectId = Option(diff.getNewId).map(_.name),
tooLarge = false
)
}
}
}.toList
}
@@ -713,7 +779,7 @@ object JGitUtil {
def getContentFromId(git: Git, id: ObjectId, fetchLargeFile: Boolean): Option[Array[Byte]] = try {
using(git.getRepository.getObjectDatabase){ db =>
val loader = db.open(id)
if(fetchLargeFile == false && FileUtil.isLarge(loader.getSize)){
if(loader.isLarge || (fetchLargeFile == false && FileUtil.isLarge(loader.getSize))){
None
} else {
Some(loader.getBytes)
@@ -723,6 +789,22 @@ object JGitUtil {
case e: MissingObjectException => None
}
/**
* Get objectLoader of the given object id from the Git repository.
*
* @param git the Git object
* @param id the object id
* @param f the function process ObjectLoader
* @return None if object does not exist
*/
def getObjectLoaderFromId[A](git: Git, id: ObjectId)(f: ObjectLoader => A):Option[A] = try {
using(git.getRepository.getObjectDatabase){ db =>
Some(f(db.open(id)))
}
} catch {
case e: MissingObjectException => None
}
/**
* Returns all commit id in the specified repository.
*/

View File

@@ -37,7 +37,7 @@ trait Notifier extends RepositoryService with AccountService with IssuesService
object Notifier {
// TODO We want to be able to switch to mock.
def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match {
case settings if settings.notification => new Mailer(settings.smtp.get)
case settings if (settings.notification && settings.useSMTP) => new Mailer(settings.smtp.get)
case _ => new MockMailer
}

View File

@@ -20,7 +20,7 @@ object StringUtil {
md.digest.map(b => "%02x".format(b)).mkString
}
def urlEncode(value: String): String = URLEncoder.encode(value, "UTF-8")
def urlEncode(value: String): String = URLEncoder.encode(value, "UTF-8").replace("+", "%20")
def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8")

View File

@@ -7,31 +7,92 @@ import gitbucket.core.util.Implicits.RichString
trait LinkConverter { self: RequestCache =>
/**
* Converts issue id, username and commit id to link.
* Creates a link to the issue or the pull request from the issue id.
*/
protected def convertRefsLinks(value: String, repository: RepositoryService.RepositoryInfo,
issueIdPrefix: String = "#", escapeHtml: Boolean = true)(implicit context: Context): String = {
protected def createIssueLink(repository: RepositoryService.RepositoryInfo, issueId: Int)(implicit context: Context): String = {
val userName = repository.repository.userName
val repositoryName = repository.repository.repositoryName
getIssue(userName, repositoryName, issueId.toString) match {
case Some(issue) if (issue.isPullRequest) =>
s"""<a href="${context.path}/${userName}/${repositoryName}/pull/${issueId}">Pull #${issueId}</a>"""
case Some(_) =>
s"""<a href="${context.path}/${userName}/${repositoryName}/issues/${issueId}">Issue #${issueId}</a>"""
case None =>
s"Unknown #${issueId}"
}
}
/**
* Converts issue id, username and commit id to link in the given text.
*/
protected def convertRefsLinks(text: String, repository: RepositoryService.RepositoryInfo,
issueIdPrefix: String = "#", escapeHtml: Boolean = true)(implicit context: Context): String = {
// escape HTML tags
val escaped = if(escapeHtml) value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;") else value
val escaped = if(escapeHtml) text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;") else text
escaped
// convert issue id to link
.replaceBy(("(?<=(^|\\W))" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r){ m =>
getIssue(repository.owner, repository.name, m.group(2)) match {
case Some(issue) if(issue.isPullRequest)
=> Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/pull/${m.group(2)}">#${m.group(2)}</a>""")
case Some(_) => Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/issues/${m.group(2)}">#${m.group(2)}</a>""")
case None => Some(s"""#${m.group(2)}""")
// convert username/project@SHA to link
.replaceBy("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)/([a-zA-Z0-9\\-_\\.]+)@([a-f0-9]{40})(?=(\\W|$))".r){ m =>
getAccountByUserName(m.group(2)).map { _ =>
s"""<a href="${context.path}/${m.group(2)}/${m.group(3)}/commit/${m.group(4)}">${m.group(2)}/${m.group(3)}@${m.group(4).substring(0, 7)}</a>"""
}
}
// convert username/project#Num to link
.replaceBy( ("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)/([a-zA-Z0-9\\-_\\.]+)" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r){ m =>
getIssue(m.group(2), m.group(3), m.group(4)) match {
case Some(issue) if (issue.isPullRequest) =>
Some(s"""<a href="${context.path}/${m.group(2)}/${m.group(3)}/pull/${m.group(4)}">${m.group(2)}/${m.group(3)}#${m.group(4)}</a>""")
case Some(_) =>
Some(s"""<a href="${context.path}/${m.group(2)}/${m.group(3)}/issues/${m.group(4)}">${m.group(2)}/${m.group(3)}#${m.group(4)}</a>""")
case None =>
Some(s"""${m.group(2)}/${m.group(3)}#${m.group(4)}""")
}
}
// convert username@SHA to link
.replaceBy( ("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)@([a-f0-9]{40})(?=(\\W|$))").r ) { m =>
getAccountByUserName(m.group(2)).map { _ =>
s"""<a href="${context.path}/${m.group(2)}/${repository.name}/commit/${m.group(3)}">${m.group(2)}@${m.group(3).substring(0, 7)}</a>"""
}
}
// convert username#Num to link
.replaceBy( ("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r ) { m =>
getIssue(m.group(2), repository.name, m.group(3)) match {
case Some(issue) if(issue.isPullRequest) =>
Some(s"""<a href="${context.path}/${m.group(2)}/${repository.name}/pull/${m.group(3)}">${m.group(2)}#${m.group(3)}</a>""")
case Some(_) =>
Some(s"""<a href="${context.path}/${m.group(2)}/${repository.name}/issues/${m.group(3)}">${m.group(2)}#${m.group(3)}</a>""")
case None =>
Some(s"""${m.group(2)}#${m.group(3)}""")
}
}
// convert issue id to link
.replaceBy(("(?<=(^|\\W))(GH-|" + issueIdPrefix + ")([0-9]+)(?=(\\W|$))").r){ m =>
val prefix = if(m.group(2) == "issue:") "#" else m.group(2)
getIssue(repository.owner, repository.name, m.group(3)) match {
case Some(issue) if(issue.isPullRequest) =>
Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/pull/${m.group(3)}">${prefix}${m.group(3)}</a>""")
case Some(_) =>
Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/issues/${m.group(3)}">${prefix}${m.group(3)}</a>""")
case None =>
Some(s"""${m.group(2)}${m.group(3)}""")
}
}
// convert @username to link
.replaceBy("(?<=(^|\\W))@([a-zA-Z0-9\\-_]+)(?=(\\W|$))".r){ m =>
.replaceBy("(?<=(^|\\W))@([a-zA-Z0-9\\-_\\.]+)(?=(\\W|$))".r){ m =>
getAccountByUserName(m.group(2)).map { _ =>
s"""<a href="${context.path}/${m.group(2)}">@${m.group(2)}</a>"""
}
}
// convert commit id to link
.replaceAll("(?<=(^|\\W))([a-f0-9]{40})(?=(\\W|$))", s"""<a href="${context.path}/${repository.owner}/${repository.name}/commit/$$2">$$2</a>""")
.replaceAll("(?<=(^|[^\\w/@]))([a-f0-9]{40})(?=(\\W|$))", s"""<a href="${context.path}/${repository.owner}/${repository.name}/commit/$$2">$$2</a>""")
}
}

View File

@@ -1,18 +1,14 @@
package gitbucket.core.view
import java.text.Normalizer
import java.util.Locale
import java.util.regex.Pattern
import java.util.Locale
import gitbucket.core.controller.Context
import gitbucket.core.service.{RepositoryService, RequestCache, WikiService}
import gitbucket.core.service.{RepositoryService, RequestCache}
import gitbucket.core.util.StringUtil
import org.parboiled.common.StringUtils
import org.pegdown.LinkRenderer.Rendering
import org.pegdown._
import org.pegdown.ast._
import scala.collection.JavaConverters._
import io.github.gitbucket.markedj._
import io.github.gitbucket.markedj.Utils._
object Markdown {
@@ -24,7 +20,7 @@ object Markdown {
* @param enableRefsLink if true then issue reference (e.g. #123) is rendered as link
* @param enableAnchor if true then anchor for headline is generated
* @param enableTaskList if true then task list syntax is available
* @param hasWritePermission
* @param hasWritePermission true if user has writable to ths given repository
* @param pages the list of existing Wiki pages
*/
def toHtml(markdown: String,
@@ -35,7 +31,6 @@ object Markdown {
enableTaskList: Boolean = false,
hasWritePermission: Boolean = false,
pages: List[String] = Nil)(implicit context: Context): String = {
// escape issue id
val s = if(enableRefsLink){
markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2")
@@ -43,252 +38,146 @@ object Markdown {
// escape task list
val source = if(enableTaskList){
GitBucketHtmlSerializer.escapeTaskList(s)
escapeTaskList(s)
} else s
val rootNode = new PegDownProcessor(
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS |
Extensions.TABLES | Extensions.HARDWRAPS | Extensions.SUPPRESS_ALL_HTML | Extensions.STRIKETHROUGH
).parseMarkdown(source.toCharArray)
new GitBucketHtmlSerializer(
markdown, repository, enableWikiLink, enableRefsLink, enableAnchor, enableTaskList,
hasWritePermission, pages
).toHtml(rootNode)
val options = new Options()
options.setSanitize(true)
val renderer = new GitBucketMarkedRenderer(options, repository, enableWikiLink, enableRefsLink, enableAnchor, enableTaskList, hasWritePermission, pages)
Marked.marked(source, options, renderer)
}
}
class GitBucketLinkRender(
context: Context,
repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
pages: List[String]) extends LinkRenderer with WikiService {
/**
* Extends markedj Renderer for GitBucket
*/
class GitBucketMarkedRenderer(options: Options, repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean, enableAnchor: Boolean, enableTaskList: Boolean, hasWritePermission: Boolean,
pages: List[String])
(implicit val context: Context) extends Renderer(options) with LinkConverter with RequestCache {
override def render(node: WikiLinkNode): Rendering = {
if(enableWikiLink){
try {
val text = node.getText
val (label, page) = if(text.contains('|')){
val i = text.indexOf('|')
(text.substring(0, i), text.substring(i + 1))
override def heading(text: String, level: Int, raw: String): String = {
val id = generateAnchorName(text)
val out = new StringBuilder()
out.append("<h" + level + " id=\"" + options.getHeaderPrefix + id + "\" class=\"markdown-head\">")
if(enableAnchor){
out.append("<a class=\"markdown-anchor-link\" href=\"#" + id + "\"></a>")
out.append("<a class=\"markdown-anchor\" name=\"" + id + "\"></a>")
}
out.append(text)
out.append("</h" + level + ">\n")
out.toString()
}
override def code(code: String, lang: String, escaped: Boolean): String = {
"<pre class=\"prettyprint" + (if(lang != null) s" ${options.getLangPrefix}${lang}" else "" )+ "\">" +
(if(escaped) code else escape(code, true)) + "</pre>"
}
override def list(body: String, ordered: Boolean): String = {
var listType: String = null
if (ordered) {
listType = "ol"
}
else {
listType = "ul"
}
if(body.contains("""class="task-list-item-checkbox"""")){
return "<" + listType + " class=\"task-list\">\n" + body + "</" + listType + ">\n"
} else {
return "<" + listType + ">\n" + body + "</" + listType + ">\n"
}
}
override def listitem(text: String): String = {
if(text.contains("""class="task-list-item-checkbox" """)){
return "<li class=\"task-list-item\">" + text + "</li>\n"
} else {
return "<li>" + text + "</li>\n"
}
}
override def text(text: String): String = {
// convert commit id and username to link.
val t1 = if(enableRefsLink) convertRefsLinks(text, repository, "issue:", false) else text
// convert task list to checkbox.
val t2 = if(enableTaskList) convertCheckBox(t1, hasWritePermission) else t1
t2
}
override def link(href: String, title: String, text: String): String = {
super.link(fixUrl(href, false), title, text)
}
override def image(href: String, title: String, text: String): String = {
super.image(fixUrl(href, true), title, text)
}
override def nolink(text: String): String = {
if(enableWikiLink && text.startsWith("[[") && text.endsWith("]]")){
val link = text.replaceAll("(^\\[\\[|\\]\\]$)", "")
val (label, page) = if(link.contains('|')){
val i = link.indexOf('|')
(link.substring(0, i), link.substring(i + 1))
} else {
(text, text)
(link, link)
}
val url = repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/" + StringUtil.urlEncode(page)
if(pages.contains(page)){
new Rendering(url, label)
"<a href=\"" + url + "\">" + escape(label) + "</a>"
} else {
new Rendering(url, label).withAttribute("class", "absent")
"<a href=\"" + url + "\" class=\"absent\">" + escape(label) + "</a>"
}
} catch {
case e: java.io.UnsupportedEncodingException => throw new IllegalStateException
}
} else {
super.render(node)
}
}
}
class GitBucketVerbatimSerializer extends VerbatimSerializer {
def serialize(node: VerbatimNode, printer: Printer): Unit = {
printer.println.print("<pre")
if (!StringUtils.isEmpty(node.getType)) {
printer.print(" class=").print('"').print("prettyprint ").print(node.getType).print('"')
}
printer.print(">")
var text: String = node.getText
while (text.charAt(0) == '\n') {
printer.print("<br/>")
text = text.substring(1)
}
printer.printEncoded(text)
printer.print("</pre>")
}
}
class GitBucketHtmlSerializer(
markdown: String,
repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
enableRefsLink: Boolean,
enableAnchor: Boolean,
enableTaskList: Boolean,
hasWritePermission: Boolean,
pages: List[String]
)(implicit val context: Context) extends ToHtmlSerializer(
new GitBucketLinkRender(context, repository, enableWikiLink, pages),
Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava
) with LinkConverter with RequestCache {
override protected def printImageTag(rendering: LinkRenderer.Rendering): Unit = {
printer.print("<a target=\"_blank\" href=\"").print(fixUrl(rendering.href, true)).print("\">")
.print("<img src=\"").print(fixUrl(rendering.href, true)).print("\" alt=\"").printEncoded(rendering.text).print("\"/></a>")
}
override protected def printLink(rendering: LinkRenderer.Rendering): Unit = {
printer.print('<').print('a')
printAttribute("href", fixUrl(rendering.href))
for (attr <- rendering.attributes.asScala) {
printAttribute(attr.name, attr.value)
}
printer.print('>').print(rendering.text).print("</a>")
}
private def fixUrl(url: String, isImage: Boolean = false): String = {
if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("/")){
url
} else if(url.startsWith("#")){
("#" + GitBucketHtmlSerializer.generateAnchorName(url.substring(1)))
} else if(!enableWikiLink){
if(context.currentPath.contains("/blob/")){
url + (if(isImage) "?raw=true" else "")
} else if(context.currentPath.contains("/tree/")){
val paths = context.currentPath.split("/")
val branch = if(paths.length > 3) paths.drop(4).mkString("/") else repository.repository.defaultBranch
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "")
} else {
val paths = context.currentPath.split("/")
val branch = if(paths.length > 3) paths.last else repository.repository.defaultBranch
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "")
escape(text)
}
} else {
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url
}
}
private def printAttribute(name: String, value: String): Unit = {
printer.print(' ').print(name).print('=').print('"').print(value).print('"')
}
private def printHeaderTag(node: HeaderNode): Unit = {
val tag = s"h${node.getLevel}"
val child = node.getChildren.asScala.headOption
val anchorName = child match {
case Some(x: AnchorLinkNode) => x.getName
case Some(x: TextNode) => x.getText
case _ => GitBucketHtmlSerializer.generateAnchorName(extractText(node)) // TODO
}
printer.print(s"""<$tag class="markdown-head">""")
if(enableAnchor){
printer.print(s"""<a class="markdown-anchor-link" href="#$anchorName"></a>""")
printer.print(s"""<a class="markdown-anchor" name="$anchorName"></a>""")
private def fixUrl(url: String, isImage: Boolean = false): String = {
if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("/")){
url
} else if(url.startsWith("#")){
("#" + generateAnchorName(url.substring(1)))
} else if(!enableWikiLink){
if(context.currentPath.contains("/blob/")){
url + (if(isImage) "?raw=true" else "")
} else if(context.currentPath.contains("/tree/")){
val paths = context.currentPath.split("/")
val branch = if(paths.length > 3) paths.drop(4).mkString("/") else repository.repository.defaultBranch
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "")
} else {
val paths = context.currentPath.split("/")
val branch = if(paths.length > 3) paths.last else repository.repository.defaultBranch
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "")
}
} else {
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url
}
}
child match {
case Some(x: AnchorLinkNode) => printer.print(x.getText)
case _ => visitChildren(node)
}
printer.print(s"</$tag>")
}
private def extractText(node: Node): String = {
val sb = new StringBuilder()
node.getChildren.asScala.map {
case x: TextNode => sb.append(x.getText)
case x: Node => sb.append(extractText(x))
}
sb.toString()
}
override def visit(node: HeaderNode): Unit = {
printHeaderTag(node)
}
override def visit(node: TextNode): Unit = {
// convert commit id and username to link.
val t = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText
// convert task list to checkbox.
val text = if(enableTaskList) GitBucketHtmlSerializer.convertCheckBox(t, hasWritePermission) else t
if (abbreviations.isEmpty) {
printer.print(text)
} else {
printWithAbbreviations(text)
}
}
override def visit(node: VerbatimNode) {
val printer = new Printer()
val serializer = verbatimSerializers.get(VerbatimSerializer.DEFAULT)
serializer.serialize(node, printer)
val html = printer.getString
// convert commit id and username to link.
val t = if(enableRefsLink) convertRefsLinks(html, repository, "issue:", escapeHtml = false) else html
this.printer.print(t)
}
override def visit(node: BulletListNode): Unit = {
if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) {
printer.println().print("""<ul class="task-list">""").indent(+2)
visitChildren(node)
printer.indent(-2).println().print("</ul>")
} else {
printIndentedTag(node, "ul")
}
}
override def visit(node: ListItemNode): Unit = {
if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) {
printer.println()
printer.print("""<li class="task-list-item">""")
visitChildren(node)
printer.print("</li>")
} else {
printer.println()
printTag(node, "li")
}
}
override def visit(node: ExpLinkNode) {
printLink(linkRenderer.render(node, printLinkChildrenToString(node)))
}
def printLinkChildrenToString(node: SuperNode) = {
val priorPrinter = printer
printer = new Printer()
visitLinkChildren(node)
val result = printer.getString()
printer = priorPrinter
result
}
def visitLinkChildren(node: SuperNode) {
import scala.collection.JavaConversions._
node.getChildren.foreach(child => child match {
case node: ExpImageNode => visitLinkChild(node)
case node: SuperNode => visitLinkChildren(node)
case _ => child.accept(this)
})
}
def visitLinkChild(node: ExpImageNode) {
printer.print("<img src=\"").print(fixUrl(node.url, true)).print("\" alt=\"").printEncoded(printChildrenToString(node)).print("\"/>")
}
}
object GitBucketHtmlSerializer {
private val Whitespace = "[\\s]".r
def generateAnchorName(text: String): String = {
val noWhitespace = Whitespace.replaceAllIn(text, "-")
val normalized = Normalizer.normalize(noWhitespace, Normalizer.Form.NFD)
val noSpecialChars = StringUtil.urlEncode(normalized)
noSpecialChars.toLowerCase(Locale.ENGLISH)
}
def escapeTaskList(text: String): String = {
Pattern.compile("""^( *)- \[([x| ])\] """, Pattern.MULTILINE).matcher(text).replaceAll("$1* task:$2: ")
}
def generateAnchorName(text: String): String = {
val normalized = Normalizer.normalize(text.replaceAll("<.*>", "").replaceAll("[\\s]", "-"), Normalizer.Form.NFD)
val encoded = StringUtil.urlEncode(normalized)
encoded.toLowerCase(Locale.ENGLISH)
}
def convertCheckBox(text: String, hasWritePermission: Boolean): String = {
val disabled = if (hasWritePermission) "" else "disabled"
text.replaceAll("task:x:", """<input type="checkbox" class="task-list-item-checkbox" checked="checked" """ + disabled + "/>")
.replaceAll("task: :", """<input type="checkbox" class="task-list-item-checkbox" """ + disabled + "/>")
.replaceAll("task: :", """<input type="checkbox" class="task-list-item-checkbox" """ + disabled + "/>")
}
}

View File

@@ -91,8 +91,12 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
enableTaskList: Boolean = false,
hasWritePermission: Boolean = false,
pages: List[String] = Nil)(implicit context: Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, true, hasWritePermission, pages))
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, true, enableTaskList, hasWritePermission, pages))
/**
* Render the given source (only markdown is supported in default) as HTML.
* You can test if a file is renderable in this method by [[isRenderable()]].
*/
def renderMarkup(filePath: List[String], fileContent: String, branch: String,
repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean, enableAnchor: Boolean)(implicit context: Context): Html = {
@@ -103,10 +107,20 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
renderer.render(RenderRequest(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, enableAnchor, context))
}
/**
* Tests whether the given file is renderable. It's tested by the file extension.
*/
def isRenderable(fileName: String): Boolean = {
PluginRegistry().renderableExtensions.exists(extension => fileName.toLowerCase.endsWith("." + extension))
}
/**
* Creates a link to the issue or the pull request from the issue id.
*/
def issueLink(repository: RepositoryService.RepositoryInfo, issueId: Int)(implicit context: Context): Html = {
Html(createIssueLink(repository, issueId))
}
/**
* Returns &lt;img&gt; which displays the avatar icon for the given user name.
* This method looks up Gravatar if avatar icon has not been configured in user settings.
@@ -275,4 +289,11 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
case CommitState.ERROR => "Failed"
case CommitState.FAILURE => "Failed"
}
// This pattern comes from: http://stackoverflow.com/a/4390768/1771641 (extract-url-from-string)
private[this] val detectAndRenderLinksRegex = """(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,13}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""".r
def detectAndRenderLinks(text: String): Html = {
Html(detectAndRenderLinksRegex.replaceAllIn(text, m => s"""<a href="${m.group(0)}">${m.group(0)}</a>"""))
}
}

View File

@@ -14,9 +14,9 @@
</div>
<div class="block">
@if(account.url.isDefined){
<div><i class="icon-home"></i> <a href="@account.url">@account.url</a></div>
<div><i class="octicon octicon-home"></i> <a href="@account.url">@account.url</a></div>
}
<div><i class="icon-time"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
<div><i class="octicon octicon-clock"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
</div>
@if(groupNames.nonEmpty){
<div>

View File

@@ -13,7 +13,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a href="javascript:void(0);" data-name="@loginAccount.get.userName"><i class="icon-ok"></i> <span>@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</span></a></li>
<li><a href="javascript:void(0);" data-name="@loginAccount.get.userName"><i class="octicon octicon-check"></i> <span>@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</span></a></li>
@groupNames.map { groupName =>
<li><a href="javascript:void(0);" data-name="@groupName"><i class="icon-white"></i> <span>@avatar(groupName, 20) @groupName</span></a></li>
}
@@ -40,7 +40,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
<fieldset>
<label class="radio">
<input type="radio" name="isPrivate" value="true" @if(!isCreateRepoOptionPublic){checked}>
<span class="strong"><i class="icon-lock"></i>&nbsp;</i>&nbsp;Private</span><br>
<span class="strong"><i class="octicon octicon-lock"></i>&nbsp;</i>&nbsp;Private</span><br>
<div>
<span>Only collaborators can read this repository.</span>
</div>

View File

@@ -16,7 +16,7 @@
<div class="block-header">
<a href="@url(repository)">@repository.name</a>
@if(repository.repository.isPrivate){
<i class="icon-lock"></i>
<i class="octicon octicon-lock"></i>
}
</div>
@if(repository.repository.originUserName.isDefined){

View File

@@ -3,19 +3,20 @@
<div class="container">
<div class="row-fluid">
<div class="span3">
<div class="box">
<ul class="nav nav-tabs nav-stacked side-menu">
<li@if(active=="users"){ class="active"}>
<a href="@path/admin/users">User Management</a>
</li>
<li@if(active=="system"){ class="active"}>
<a href="@path/admin/system">System Settings</a>
</li>
<li>
<a href="@path/console/login.jsp">H2 Console</a>
</li>
</ul>
</div>
<ul class="nav nav-tabs nav-stacked side-menu" id="system-admin-menu-container">
<li@if(active=="users"){ class="active"}>
<a href="@path/admin/users">User Management</a>
</li>
<li@if(active=="system"){ class="active"}>
<a href="@path/admin/system">System Settings</a>
</li>
<li@if(active=="plugins"){ class="active"}>
<a href="@path/admin/plugins">Plugins</a>
</li>
<li>
<a href="@path/console/login.jsp">H2 Console</a>
</li>
</ul>
</div>
<div class="span9">
@body

View File

@@ -0,0 +1,30 @@
@(plugins: List[gitbucket.core.plugin.PluginInfo])(implicit context: gitbucket.core.controller.Context)
@import context._
@import gitbucket.core.view.helpers._
@html.main("Plugins"){
@admin.html.menu("plugins") {
<h1>Installed plugins</h1>
@if(plugins.size > 0) {
<ul>
@plugins.map {plugin =>
<li><a href="#@plugin.pluginId">@plugin.pluginId:@plugin.version</a></li>
}
</ul>
@plugins.map {plugin =>
<div class="box">
<div class="box-header">@plugin.pluginName</div>
<div class="box-content">
<p><span class="strong">Id:&nbsp;</span>@plugin.pluginId</p>
<p><span class="strong">Version:&nbsp;</span>@plugin.version</p>
<p><span class="strong">Name:&nbsp;</span>@plugin.pluginName</p>
<p class="muted">@plugin.description</p>
</div>
</div>
}
} else {
<p>No plugin detected on your gitbucket installation.</p>
}
}
}

View File

@@ -224,14 +224,25 @@
<!-- Notification email -->
<!--====================================================================-->
<hr>
<label class="strong">Notification email</label>
<label class="strong">Notifications</label>
<fieldset>
<label class="checkbox">
<input type="checkbox" id="notification" name="notification"@if(settings.notification){ checked}/>
Send notifications
</label>
</fieldset>
<div class="form-horizontal notification">
<!--====================================================================-->
<!-- Communication email -->
<!--====================================================================-->
<hr>
<label class="strong">Communication</label>
<fieldset>
<label class="checkbox">
<input type="checkbox" id="useSMTP" name="useSMTP" @if(settings.useSMTP){ checked}/>
SMTP
</label>
</fieldset>
<div class="form-horizontal useSMTP">
<div class="control-group">
<label class="control-label" for="smtpHost">SMTP Host</label>
<div class="controls">
@@ -277,12 +288,15 @@
<input type="text" id="fromName" name="smtp.fromName" value="@settings.smtp.map(_.fromName)"/>
</div>
</div>
<p class="muted">
Enable notification not only SMTP configuration if you want to send nitification email.
</p>
</div>
</div>
</div>
<fieldset>
<div class="align-right" style="margin-top: 20px;">
<input type="submit" class="btn btn-success" value="Apply changes"/>
</fieldset>
</div>
</form>
}
}
@@ -292,8 +306,16 @@ $(function(){
$('.ssh input').prop('disabled', !$(this).prop('checked'));
}).change();
$('#notification').change(function(){
$('.notification input').prop('disabled', !$(this).prop('checked'));
$('#useSMTP').change(function(){
$('.useSMTP input').prop('disabled', !$(this).prop('checked'));
// With only SMTP in current version, notification cannot be enabled if no communication channel exists
$('#notification').prop('disabled', !$(this).prop('checked'));
if (!$(this).prop('checked')) {
// With only SMTP in current version, if SMTP is unchecked then we disable notification
$('#notification').prop('checked', false);
}
}).change();
$('#ldapAuthentication').change(function(){

View File

@@ -43,10 +43,10 @@
<div>
<hr>
@if(!account.isGroupAccount){
<i class="icon-envelope"></i> @account.mailAddress
<i class="octicon octicon-mail"></i> @account.mailAddress
}
@account.url.map { url =>
<i class="icon-home"></i> @url
<i class="octicon octicon-home"></i> @url
}
</div>
<div>

View File

@@ -1,22 +1,24 @@
@(owner: String, repository: String)(textarea: Html)(implicit context: gitbucket.core.controller.Context)
@import context._
@import gitbucket.core.util.FileUtil
<div class="muted attachable">
@textarea
<div class="clickable">Attach images by dragging &amp; dropping, or selecting them.</div>
<div class="clickable">Attach images or documents by dragging &amp; dropping, or selecting them.</div>
</div>
@defining("(id=\")([\\w\\-]*)(\")".r.findFirstMatchIn(textarea.body).map(_.group(2))){ textareaId =>
<script>
$(function(){
try {
$([$('#@textareaId').closest('div')[0], $('#@textareaId').next('div')[0]]).dropzone({
url: '@path/upload/image/@owner/@repository',
url: '@path/upload/file/@owner/@repository',
maxFilesize: 10,
acceptedFiles: 'image/*',
dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, or JPG.',
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your images...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
acceptedFiles: @Html(FileUtil.mimeTypeWhiteList.mkString("'", ",", "'")),
dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, JPG, DOCX, PPTX, XLSX, TXT, or PDF.',
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
success: function(file, id) {
var images = '\n![' + file.name.split('.')[0] + '](@baseUrl/@owner/@repository/_attached/' + id + ')';
$('#@textareaId').val($('#@textareaId').val() + images);
var attachFile = (file.type.match(/image\/.*/) ? '\n![' + file.name.split('.')[0] : '\n[' + file.name) +
'](@baseUrl/@owner/@repository/_attached/' + id + ')';
$('#@textareaId').val($('#@textareaId').val() + attachFile);
$(file.previewElement).prevAll('div.dz-preview').addBack().remove();
}
});

View File

@@ -8,7 +8,7 @@
@helper.html.dropdown(
value = if(branch.length == 40) branch.substring(0, 10) else branch,
prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree",
mini = true
mini = false
) {
<li><div id="branch-control-title">Switch branches<button id="branch-control-close" class="pull-right">&times</button></div></li>
<li><input id="branch-control-input" type="text" placeholder="Find or create branch ..."/></li>

View File

@@ -1,6 +1,6 @@
@(condition: => Boolean)
@if(condition){
<i class="icon-ok"></i>
<i class="octicon octicon-check"></i>
} else {
<i class="icon-white"></i>
}

View File

@@ -5,10 +5,13 @@
@import context._
@import gitbucket.core._
@import gitbucket.core.view.helpers._
<div class="@if(comment.fileName.isDefined && (!latestCommitId.isDefined || latestCommitId.get == comment.commitId)){inline-comment}" @if(comment.fileName.isDefined){filename=@comment.fileName.get} @if(comment.newLine.isDefined){newline=@comment.newLine.get} @if(comment.oldLine.isDefined){oldline=@comment.oldLine.get}>
<div class="@if(comment.fileName.isDefined && (!latestCommitId.isDefined || latestCommitId.get == comment.commitId)){inline-comment}"
@if(comment.fileName.isDefined){filename="@comment.fileName.get"}
@if(comment.newLine.isDefined){newline="@comment.newLine.get"}
@if(comment.oldLine.isDefined){oldline="@comment.oldLine.get"}>
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
<div class="box commit-comment-box commit-comment-@comment.commentId">
<div class="box-header-small">
<div class="box-header">
@user(comment.commentedUserName, styleClass="username strong")
<span class="muted">
commented
@@ -24,12 +27,12 @@
</span>
<span class="pull-right">
@if(hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false)){
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>&nbsp;
<a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle"></i></a>
<a href="#" data-comment-id="@comment.commentId"><i class="octicon octicon-pencil"></i></a>&nbsp;
<a href="#" data-comment-id="@comment.commentId"><i class="octicon octicon-x"></i></a>
}
</span>
</div>
<div class="box-content commit-commentContent-@comment.commentId markdown-body">
<div class="box-content-bottom commit-commentContent-@comment.commentId markdown-body">
@markdown(comment.content, repository, false, true, true, hasWritePermission)
</div>
</div>

View File

@@ -1,7 +1,7 @@
@(id: String, value: String)(html: Html)
<div class="input-append" style="margin-bottom: 0px;">
@(id: String, value: String, prepend: Boolean = false)(html: Html)
<div class="input-append @if(prepend){input-prepend}" style="margin-bottom: 0px;">
@html
<span id="@id" class="add-on btn" data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="icon-check"></i></span>
<span id="@id" class="add-on btn" data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="octicon octicon-clippy"></i></span>
</div>
<script>
// copy to clipboard

View File

@@ -2,7 +2,7 @@
@import gitbucket.core.view.helpers
<div id="@name" class="input-append date" data-date-format="yyyy-mm-dd" data-date="@value.map(helpers.date)">
<input class="span2" name="@name" type="text" readonly="" value="@value.map(helpers.date)" size="16"/>
<span class="add-on"><i class="icon-calendar"></i></span>
<span class="add-on"><i class="octicon octicon-calendar"></i></span>
</div>
<script>
$(function(){

View File

@@ -92,30 +92,38 @@
<td style="padding: 0;">
@if(diff.oldObjectId == diff.newObjectId){
<div class="diff-same">File renamed without changes</div>
} else { @if(diff.newContent != None || diff.oldContent != None){
<div id="diffText-@i" class="diffText"></div>
<textarea id="newText-@i" style="display: none;" data-file-name="@diff.oldPath">@diff.newContent.getOrElse("")</textarea>
<textarea id="oldText-@i" style="display: none;" data-file-name="@diff.newPath">@diff.oldContent.getOrElse("")</textarea>
} else { @if(diff.newIsImage || diff.oldIsImage){
<div class="diff-image-render diff2up">
@if(oldCommitId.isDefined && diff.oldIsImage){
<div class="diff-image-frame diff-old"><img src="@url(repository)/blob/@oldCommitId.get/@diff.oldPath?raw=true" class="diff-image" onload="onLoadedDiffImages(this)" style="display:none" /></div>
} else {
@if(diff.changeType != ChangeType.ADD){
Not supported
}
}
@if(newCommitId.isDefined && diff.newIsImage){
<div class="diff-image-frame diff-new"><img src="@url(repository)/blob/@newCommitId.get/@diff.newPath?raw=true" class="diff-image" onload="onLoadedDiffImages(this)" style="display:none" /></div>
} else {
@if(diff.changeType != ChangeType.DELETE){
Not supported
}
}
</div>
} else {
Not supported
} } }
@if(diff.newContent != None || diff.oldContent != None){
<div id="diffText-@i" class="diffText"></div>
<textarea id="newText-@i" style="display: none;" data-file-name="@diff.oldPath">@diff.newContent.getOrElse("")</textarea>
<textarea id="oldText-@i" style="display: none;" data-file-name="@diff.newPath">@diff.oldContent.getOrElse("")</textarea>
} else {
@if(diff.newIsImage || diff.oldIsImage){
<div class="diff-image-render diff2up">@diff.oldIsImage @diff.newIsImage
@if(oldCommitId.isDefined && diff.oldIsImage){
<div class="diff-image-frame diff-old"><img src="@url(repository)/blob/@oldCommitId.get/@diff.oldPath?raw=true" class="diff-image" onload="onLoadedDiffImages(this)" style="display:none" /></div>
} else {
@if(diff.changeType != ChangeType.ADD){
<div style="padding: 12px;">Not supported</div>
}
}
@if(newCommitId.isDefined && diff.newIsImage){
<div class="diff-image-frame diff-new"><img src="@url(repository)/blob/@newCommitId.get/@diff.newPath?raw=true" class="diff-image" onload="onLoadedDiffImages(this)" style="display:none" /></div>
} else {
@if(diff.changeType != ChangeType.DELETE){
<div style="padding: 12px;">Not supported</div>
}
}
</div>
} else {
@if(diff.tooLarge){
<div style="padding: 12px;">Too large</div>
} else {
<div style="padding: 12px;">Not supported</div>
}
}
}
}
</td>
</tr>
</table>

View File

@@ -9,7 +9,7 @@
@if(flat){style="border: none; background-color: #eee;"}
class="dropdown-toggle @if(!flat){btn} else {flat} @if(mini){btn-mini} else {btn-small}" data-toggle="dropdown">
@if(value.isEmpty){
<i class="icon-cog"></i>
<i class="octicon octicon-gear"></i>
} else {
@if(prefix.nonEmpty){
<span class="muted">@prefix:</span>

View File

@@ -8,6 +8,7 @@
styleClass: String = "",
placeholder: String = "Leave a comment",
elastic: Boolean = false,
tabIndex: Int = -2,
uid: Long = new java.util.Date().getTime())(implicit context: gitbucket.core.controller.Context)
@import context._
@import gitbucket.core._
@@ -22,6 +23,7 @@
<span id="error-content" class="error"></span>
@textarea = {
<textarea id="content@uid" name="content" placeholder="@placeholder"
@if(tabIndex > -2){ tabindex="@tabIndex"}
@if(style.nonEmpty){ style="@style"}
@if(styleClass.nonEmpty){ class="@styleClass" }>@content</textarea>
}

View File

@@ -24,57 +24,64 @@
@if(loginAccount.isEmpty){
@signinform(settings)
} else {
<table class="table table-bordered">
<tr>
<th class="metal">
<div class="pull-right">
<a href="@path/new" class="btn btn-success btn-mini">New repository</a>
</div>
Your repositories (@userRepositories.size)
</th>
</tr>
<div class="box-header">
<div class="pull-right">
<a href="@path/new" class="btn btn-success btn-mini">New repository</a>
</div>
<span class="strong">Your repositories</span> <span class="label">@userRepositories.size</span>
</div>
@if(userRepositories.isEmpty){
<tr>
<td>No repositories</td>
</tr>
<div class="box-content-bottom">
No repositories
</div>
} else {
@userRepositories.map { repository =>
<tr>
<td>
@helper.html.repositoryicon(repository, false)
@if(repository.owner == loginAccount.get.userName){
<a href="@url(repository)"><span class="strong">@repository.name</span></a>
} else {
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
}
</td>
</tr>
}
<div class="box-content-bottom" style="padding: 0px;">
@defining(3){ max =>
@userRepositories.zipWithIndex.map { case (repository, i) =>
<div class="box-content-row repo-link" style="@if(i > max - 1){display:none;}">
@helper.html.repositoryicon(repository, false)
@if(repository.owner == loginAccount.get.userName){
<a href="@url(repository)"><span class="strong">@repository.name</span></a>
} else {
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
}
</div>
}
@if(userRepositories.size > max){
<div class="box-content-row show-more">
<a href="javascript:void(0);" id="show-more-repos">Show more @{userRepositories.size - max} pages...</a>
</div>
}
}
</div>
}
</table>
}
<table class="table table-bordered">
<tr>
<th class="metal">
Recent updated repositories
</th>
</tr>
@if(recentRepositories.isEmpty){
<tr>
<td>No repositories</td>
</tr>
} else {
<div class="box-header">
<span class="strong">Recent updated repositories</span>
</div>
@if(recentRepositories.isEmpty){
<div class="box-content-bottom">
No repositories
</div>
} else {
<div class="box-content-bottom" style="padding: 0px;">
@recentRepositories.map { repository =>
<tr>
<td>
@helper.html.repositoryicon(repository, false)
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
</td>
</tr>
<div class="box-content-row repo-link">
@helper.html.repositoryicon(repository, false)
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
</div>
}
}
</table>
</div>
}
</div>
</div>
</div>
}
<script>
$(function(){
$('#show-more-repos').click(function(e){
$(e.target).parents('div.box-content-bottom').find('div.repo-link').show();
$(e.target).parents('div.show-more').remove();
});
});
</script>

View File

@@ -8,7 +8,7 @@
<hr/><br/>
<form method="POST" validate="true">
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
<div class="box issue-comment-box">
<div class="issue-comment-box">
<div class="box-content">
@helper.html.preview(
repository = repository,
@@ -17,18 +17,19 @@
enableRefsLink = true,
enableTaskList = true,
hasWritePermission = hasWritePermission,
style = "width: 635px; height: 100px; max-height: 150px;",
elastic = true
style = "",
elastic = true,
tabIndex = 1
)
<div style="text-align: right;">
<input type="hidden" name="issueId" value="@issue.issueId"/>
@if((reopenable || !issue.closed) && (hasWritePermission || issue.openedUserName == loginAccount.get.userName)){
<input type="submit" class="btn" tabindex="3" formaction="@url(repository)/issue_comments/state" value="@{if(issue.closed) "Reopen" else "Close"}" id="action"/>
}
<input type="submit" class="btn btn-success" tabindex="2" formaction="@url(repository)/issue_comments/new" value="Comment"/>
</div>
</div>
</div>
<div class="pull-right">
<input type="hidden" name="issueId" value="@issue.issueId"/>
<input type="submit" class="btn btn-success" formaction="@url(repository)/issue_comments/new" value="Comment"/>
@if((reopenable || !issue.closed) && (hasWritePermission || issue.openedUserName == loginAccount.get.userName)){
<input type="submit" class="btn" formaction="@url(repository)/issue_comments/state" value="@{if(issue.closed) "Reopen" else "Close"}" id="action"/>
}
</div>
</form>
}
<script>

View File

@@ -8,16 +8,16 @@
@import gitbucket.core.model.CommitComment
@if(issue.isDefined){
<div class="issue-avatar-image">@avatar(issue.get.openedUserName, 48)</div>
<div class="box issue-comment-box">
<div class="box-header-small">
<div class="issue-comment-box">
<div class="box-header">
@user(issue.get.openedUserName, styleClass="username strong") <span class="muted">commented @helper.html.datetimeago(issue.get.registeredDate)</span>
<span class="pull-right">
@if(hasWritePermission || loginAccount.map(_.userName == issue.get.openedUserName).getOrElse(false)){
<a href="#" data-issue-id="@issue.get.issueId"><i class="icon-pencil" aria-label="Edit"></i></a>
<a href="#" data-issue-id="@issue.get.issueId"><i class="octicon octicon-pencil" aria-label="Edit"></i></a>
}
</span>
</div>
<div class="box-content issue-content markdown-body" id="issueContent">
<div class="box-content-bottom issue-content markdown-body" id="issueContent">
@markdown(issue.get.content getOrElse "No description provided.", repository, false, true, true, hasWritePermission)
</div>
</div>
@@ -28,7 +28,7 @@
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
<div class="box issue-comment-box" id="comment-@comment.commentId">
<div class="box-header-small">
<div class="box-header">
@user(comment.commentedUserName, styleClass="username strong")
<span class="muted">
@if(comment.action == "comment"){
@@ -41,12 +41,12 @@
<span class="pull-right">
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer"
&& (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil" aria-label="Edit"></i></a>&nbsp;
<a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle" aria-label="Remove"></i></a>
<a href="#" data-comment-id="@comment.commentId"><i class="octicon octicon-pencil" aria-label="Edit"></i></a>&nbsp;
<a href="#" data-comment-id="@comment.commentId"><i class="octicon octicon-x" aria-label="Remove"></i></a>
}
</span>
</div>
<div class="box-content"class="issue-content" id="commentContent-@comment.commentId">
<div class="box-content-bottom issue-content" id="commentContent-@comment.commentId">
@if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){
@defining(comment.content.substring(comment.content.length - 40)){ id =>
<div class="pull-right"><a href="@path/@repository.owner/@repository.name/commit/@id" class="monospace">@id.substring(0, 7)</a></div>
@@ -55,7 +55,7 @@
} else {
@if(comment.action == "refer"){
@defining(comment.content.split(":")){ case Array(issueId, rest @ _*) =>
<strong><a href="@path/@repository.owner/@repository.name/issues/@issueId">Issue #@issueId</a>: @rest.mkString(":")</strong>
<strong>@issueLink(repository, issueId.toInt): @rest.mkString(":")</strong>
}
} else {
<div class="markdown-body">@markdown(comment.content, repository, false, true, true, hasWritePermission)</div>
@@ -78,8 +78,8 @@
</div>
}
@if(comment.action == "close" || comment.action == "close_comment"){
<div class="small issue-comment-action">
<span class="label label-important">Closed</span>
<div class="issue-comment-action">
<i class="octicon octicon-circle-slash danger"></i>
@avatar(comment.commentedUserName, 20)
@if(issue.isDefined && issue.get.isPullRequest){
@user(comment.commentedUserName, styleClass="username strong") closed the pull request @helper.html.datetimeago(comment.registeredDate)
@@ -89,14 +89,14 @@
</div>
}
@if(comment.action == "reopen" || comment.action == "reopen_comment"){
<div class="small issue-comment-action">
<span class="label label-success">Reopened</span>
<div class="issue-comment-action issue-reopened">
<i class="octicon octicon-primitive-dot"></i>
@avatar(comment.commentedUserName, 20)
@user(comment.commentedUserName, styleClass="username strong") reopened the issue @helper.html.datetimeago(comment.registeredDate)
</div>
}
@if(comment.action == "delete_branch"){
<div class="small issue-comment-action">
<div class="issue-comment-action">
<span class="label">Deleted</span>
@avatar(comment.commentedUserName, 20)
@user(comment.commentedUserName, styleClass="username strong") deleted the <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> branch @helper.html.datetimeago(comment.registeredDate)
@@ -110,7 +110,7 @@
<script>
$(function(){
@if(issue.isDefined){
$('.issue-comment-box i.icon-pencil').click(function(){
$('.issue-comment-box i.octicon-pencil').click(function(){
var id = $(this).closest('a').data('comment-id');
var url = '@url(repository)/issue_comments/_data/' + id;
var $content = $('#commentContent-' + id);
@@ -130,7 +130,7 @@ $(function(){
});
return false;
});
$('.issue-comment-box i.icon-remove-circle').click(function(){
$('.issue-comment-box i.octicon-x').click(function(){
if(confirm('Are you sure you want to delete this?')) {
var id = $(this).closest('a').data('comment-id');
$.post('@url(repository)/issue_comments/delete/' + id,
@@ -144,7 +144,7 @@ $(function(){
return false;
});
}
$(document).on('click', '.commit-comment-box i.icon-pencil', function(){
$(document).on('click', '.commit-comment-box i.octicon-pencil', function(){
var id = $(this).closest('a').data('comment-id');
var url = '@url(repository)/commit_comments/_data/' + id;
var $content = $('.commit-commentContent-' + id, $(this).closest('.box'));
@@ -158,7 +158,7 @@ $(function(){
});
return false;
});
$(document).on('click', '.commit-comment-box i.icon-remove-circle', function(){
$(document).on('click', '.commit-comment-box i.octicon-x', function(){
if(confirm('Are you sure you want to delete this?')) {
var id = $(this).closest('a').data('comment-id');
$.post('@url(repository)/commit_comments/delete/' + id,

View File

@@ -10,148 +10,33 @@
@navigation("issues", false, repository)
<br/><br/><hr style="margin-bottom: 10px;">
<form action="@url(repository)/issues/new" method="POST" validate="true">
<div class="row-fluid">
<div class="span9">
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
<div class="box issue-box">
<div class="box-content">
<span id="error-title" class="error"></span>
<input type="text" name="title" value="" placeholder="Title" style="width: 565px;" autofocus/>
<div>
<span id="label-assigned">No one is assigned</span>
@if(hasWritePermission){
<input type="hidden" name="assignedUserName" value=""/>
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
@collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-while"></i>@avatar(collaborator, 20) @collaborator</a></li>
}
}
}
<div class="pull-right">
<span id="label-milestone">No milestone</span>
@if(hasWritePermission){
<input type="hidden" name="milestoneId" value=""/>
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
<li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
<i class="icon-while"></i> @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<i class="octicon octicon-alert" style="color:#BD2C00;"></i><span class="milestone-alert">Due by @date(dueDate)</span>
} else {
<span class="muted">Due by @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
}
<div class="row-fluid">
<div class="span10">
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
<div class="box issue-box">
<div class="box-content">
<span id="error-title" class="error"></span>
<input type="text" name="title" value="" placeholder="Title" style="width: 690px;" autofocus/>
@helper.html.preview(
repository = repository,
content = "",
enableWikiLink = false,
enableRefsLink = true,
enableTaskList = true,
hasWritePermission = hasWritePermission,
style = "width: 690px; height: 200px; max-height: 250px;",
elastic = true
)
<div class="align-right">
<input type="submit" class="btn btn-success" value="Submit new issue"/>
</div>
</div>
<hr>
@helper.html.preview(
repository = repository,
content = "",
enableWikiLink = false,
enableRefsLink = true,
enableTaskList = true,
hasWritePermission = hasWritePermission,
style = "width: 565px; height: 200px; max-height: 250px;",
elastic = true
)
</div>
</div>
<div class="pull-right">
<input type="submit" class="btn btn-success" value="Submit new issue"/>
<div class="span2">
@issueinfo(None, Nil, Nil, collaborators, milestones.map(x => (x, 0, 0)), labels, hasWritePermission, repository)
</div>
</div>
<div class="span3">
@if(hasWritePermission){
<span class="strong">Labels</span>
<div>
<div id="label-list">
<ul class="label-list nav nav-pills nav-stacked">
@labels.map { label =>
<li>
<a href="javascript:void(0);" class="toggle-label" data-label="@label.labelName" data-bgcolor="@label.color" data-fgcolor="@label.fontColor">
<span style="background-color: #@label.color;" class="label-color">&nbsp;&nbsp;</span>
@label.labelName
</a>
</li>
}
</ul>
<input type="hidden" name="labelNames" value=""/>
</div>
</div>
}
</div>
</div>
</form>
}
}
<script>
$(function(){
$('a.assign').click(function(){
var userName = $(this).data('name');
$('a.assign i.icon-ok').attr('class', 'icon-white');
if(userName == ''){
$('#label-assigned').text('No one will be assigned');
} else {
$('#label-assigned').html($('<span>')
.append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName))
.append(' will be assigned'));
$('a.assign[data-name=' + jqSelectorEscape(userName) + '] i').attr('class', 'icon-ok');
}
$('input[name=assignedUserName]').val(userName);
});
$('a.milestone').click(function(){
var title = $(this).data('title');
var milestoneId = $(this).data('id');
$('a.milestone i.icon-ok').attr('class', 'icon-white');
if(milestoneId == ''){
$('#label-milestone').text('No milestone');
} else {
$('#label-milestone').html($('<span class="strong">').text(title));
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
}
$('input[name=milestoneId]').val(milestoneId);
});
$('a.toggle-label').click(function(){
if($(this).data('selected') == true){
$(this).css({
'background-color': 'white',
'color' : 'black',
'font-weight' : 'normal'
});
$(this).data('selected', false);
} else {
$(this).css({
'background-color': '#' + $(this).data('bgcolor'),
'color' : '#' + $(this).data('fgcolor'),
'font-weight' : 'bold'
});
$(this).data('selected', true);
}
var labelNames = Array();
$('a.toggle-label').each(function(i, e){
if($(e).data('selected') == true){
labelNames.push($(e).data('label'));
}
});
$('input[name=labelNames]').val(labelNames.join(','));
});
});
</script>

View File

@@ -2,7 +2,7 @@
@import context._
<span id="error-edit-content-@commentId" class="error"></span>
@helper.html.attached(owner, repository){
<textarea style="width: 635px; height: 100px;" id="edit-content-@commentId">@content</textarea>
<textarea id="edit-content-@commentId">@content</textarea>
}
<div>
<input type="button" id="cancel-comment-@commentId" class="btn btn-small btn-danger" value="Cancel"/>

View File

@@ -1,7 +1,7 @@
@(content: Option[String], issueId: Int, owner: String, repository: String)(implicit context: gitbucket.core.controller.Context)
@import context._
@helper.html.attached(owner, repository){
<textarea style="width: 635px; height: 100px; max-height: 300px;" id="edit-content">@content.getOrElse("")</textarea>
<textarea id="edit-content">@content.getOrElse("")</textarea>
}
<div>
<input type="button" id="cancel-issue" class="btn btn-small btn-danger" value="Cancel"/>

View File

@@ -13,9 +13,9 @@
<div>
<div class="show-title pull-right">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a class="btn btn-small" href="#" id="edit">Edit</a>
<a class="btn" href="#" id="edit">Edit</a>
}
<a class="btn btn-small btn-success" href="@url(repository)/issues/new">New issue</a>
<a class="btn btn-success" href="@url(repository)/issues/new">New issue</a>
</div>
<div class="edit-title pull-right" style="display: none;">
<a class="btn" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a>
@@ -45,13 +45,14 @@
</span>
<br/><br/>
<hr>
<br/>
<div class="row-fluid">
<div class="span10">
@commentlist(Some(issue), comments, hasWritePermission, repository)
@commentform(issue, true, hasWritePermission, repository)
</div>
<div class="span2">
@issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
@issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
</div>
</div>
}

View File

@@ -1,4 +1,4 @@
@(issue: gitbucket.core.model.Issue,
@(issue: Option[gitbucket.core.model.Issue],
comments: List[gitbucket.core.model.Comment],
issueLabels: List[gitbucket.core.model.Label],
collaborators: List[String],
@@ -6,6 +6,7 @@
labels: List[gitbucket.core.model.Label],
hasWritePermission: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import context._
@import gitbucket.core.view.helpers._
<div style="margin-bottom: 8px;">
<span class="muted small strong">Labels</span>
@@ -22,6 +23,9 @@
</li>
}
}
@if(issue.isEmpty){
<input type="hidden" name="labelNames" value=""/>
}
</div>
}
</div>
@@ -34,11 +38,14 @@
@if(hasWritePermission){
<div class="pull-right">
@helper.html.dropdown(right = true) {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="octicon octicon-x"></i> Clear this milestone</a></li>
@milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
<li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
@helper.html.checkicon(Some(milestone.milestoneId) == issue.milestoneId) @milestone.title
@issue.map { issue =>
@helper.html.checkicon(Some(milestone.milestoneId) == issue.milestoneId)
}
@milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
@@ -58,14 +65,14 @@
}
</div>
<div id="milestone-progress-area">
@issue.milestoneId.map { milestoneId =>
@issue.flatMap(_.milestoneId).map { milestoneId =>
@milestones.collect { case (milestone, openCount, closeCount) if(milestone.milestoneId == milestoneId) =>
@issues.milestones.html.progress(openCount + closeCount, closeCount)
}
}
</div>
<span id="label-milestone">
@issue.milestoneId.map { milestoneId =>
@issue.flatMap(_.milestoneId).map { milestoneId =>
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
<span class="strong small">@milestone.title</span>
}
@@ -73,17 +80,20 @@
<span class="muted small">No milestone</span>
}
</span>
@if(issue.isEmpty){
<input type="hidden" name="milestoneId" value=""/>
}
<hr/>
<div style="margin-bottom: 8px;">
<span class="muted small strong">Assignee</span>
@if(hasWritePermission){
<div class="pull-right">
@helper.html.dropdown(right = true) {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="octicon octicon-x"></i> Clear assignee</a></li>
@collaborators.map { collaborator =>
<li>
<a href="javascript:void(0);" class="assign" data-name="@collaborator">
@helper.html.checkicon(Some(collaborator) == issue.assignedUserName)@avatar(collaborator, 20) @collaborator
@helper.html.checkicon(issue.exists(_.assignedUserName == collaborator))@avatar(collaborator, 20) @collaborator
</a>
</li>
}
@@ -92,39 +102,37 @@
}
</div>
<span id="label-assigned">
@issue.assignedUserName.map { userName =>
@issue.flatMap(_.assignedUserName).map { userName =>
@avatar(userName, 20) @user(userName, styleClass="username strong small")
}.getOrElse{
<span class="muted small">No one</span>
}
</span>
<hr/>
<div style="margin-bottom: 8px;">
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
<div class="muted small strong">@participants.size @plural(participants.size, "participant")</div>
@participants.map { participant => @avatarLink(participant, 20, tooltip = true) }
}
</div>
@if(issue.isEmpty){
<input type="hidden" name="assignedUserName" value=""/>
}
@issue.map { issue =>
<hr/>
<div style="margin-bottom: 8px;">
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
<div class="muted small strong">@participants.size @plural(participants.size, "participant")</div>
@participants.map { participant =>
@avatarLink(participant, 20, tooltip = true)
}
}
</div>
}
<script>
$(function(){
@issue.map { issue =>
$('a.toggle-label').click(function(){
var path, icon;
var i = $(this).children('i');
if(i.hasClass('icon-ok')){
path = 'delete';
icon = 'icon-white';
} else {
path = 'new';
icon = 'icon-ok';
}
var path = switchLabel($(this));
$.post('@url(repository)/issues/@issue.issueId/label/' + path,
{
labelId : $(this).data('label-id')
},
function(data){
i.removeClass().addClass(icon);
$('ul.label-list').empty().html(data);
});
{ labelId : $(this).data('label-id') },
function(data){
$('ul.label-list').empty().html(data);
}
);
return false;
});
@@ -132,42 +140,92 @@ $(function(){
var title = $(this).data('title');
var milestoneId = $(this).data('id');
$.post('@url(repository)/issues/@issue.issueId/milestone',
{
milestoneId: milestoneId
},
function(data){
console.log(data);
$('a.milestone i.icon-ok').attr('class', 'icon-white');
if(milestoneId == ''){
$('#label-milestone').html($('<span class="muted small">').text('No milestone'));
$('#milestone-progress-area').empty();
} else {
$('#label-milestone').html($('<span class="strong small">').text(title));
$('#milestone-progress-area').html(data);
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
{ milestoneId: milestoneId },
function(data){
displayMilestone(title, milestoneId, data);
}
});
);
});
$('a.assign').click(function(){
var $this = $(this);
var userName = $this.data('name');
$.post('@url(repository)/issues/@issue.issueId/assign',
{
assignedUserName: userName
},
function(){
$('a.assign i.icon-ok').attr('class', 'icon-white');
if(userName == ''){
$('#label-assigned').html($('<span class="muted small">').text('No one'));
} else {
$('#label-assigned').empty()
.append($this.find('img.avatar-mini').clone(false)).append(' ')
.append($('<a class="username strong small">').attr('href', '@context.path/' + userName).text(userName));
$('a.assign[data-name=' + jqSelectorEscape(userName) + '] i').attr('class', 'icon-ok');
{ assignedUserName: userName },
function(){
displayAssignee($this, userName);
}
);
});
}.getOrElse {
$('a.toggle-label').click(function(){
switchLabel($(this));
var labelNames = Array();
$('a.toggle-label').each(function(i, e){
if($(e).children('i').hasClass('icon-ok') == true){
labelNames.push($(e).text().trim());
}
});
$('input[name=labelNames]').val(labelNames.join(','));
$.post('@url(repository)/issues/new/label',
{ labelNames : labelNames.join(',') },
function(data){
$('ul.label-list').empty().html(data);
}
);
});
$('a.milestone').click(function(){
var title = $(this).data('title');
var milestoneId = $(this).data('id');
displayMilestone(title, milestoneId);
$('input[name=milestoneId]').val(milestoneId);
});
$('a.assign').click(function(){
var $this = $(this);
var userName = $this.data('name');
displayAssignee($this, userName);
$('input[name=assignedUserName]').val(userName);
});
}
function switchLabel($this){
var i = $this.children('i');
if(i.hasClass('icon-ok')){
i.removeClass().addClass('icon-white');
return 'delete';
} else {
i.removeClass().addClass('icon-ok');
return 'new';
}
}
function displayMilestone(title, milestoneId, progress){
$('a.milestone i.icon-ok').attr('class', 'icon-white');
if(milestoneId == ''){
$('#label-milestone').html($('<span class="muted small">').text('No milestone'));
$('#milestone-progress-area').empty();
} else {
$('#label-milestone').html($('<span class="strong small">').text(title));
if(progress){
$('#milestone-progress-area').html(progress);
}
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
}
}
function displayAssignee($this, userName){
$('a.assign i.icon-ok').attr('class', 'icon-white');
if(userName == ''){
$('#label-assigned').html($('<span class="muted small">').text('No one'));
} else {
$('#label-assigned').empty()
.append($this.find('img.avatar-mini').clone(false)).append(' ')
.append($('<a class="username strong small">').attr('href', '@context.path/' + userName).text(userName));
$('a.assign[data-name=' + jqSelectorEscape(userName) + '] i').attr('class', 'icon-ok');
}
}
});
</script>

View File

@@ -138,7 +138,7 @@
}
}
@helper.html.dropdown("Assignee", flat = true) {
<li><a href="javascript:void(0);" class="toggle-assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
<li><a href="javascript:void(0);" class="toggle-assign" data-name=""><i class="octicon octicon-x"></i> Clear assignee</a></li>
@collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li>
}

View File

@@ -41,7 +41,7 @@
<form id="search" action="@path/search" method="POST">
<div class="navbar">
<div class="navbar-inner">
<div class="container" style="width: 920px;">
<div class="container" style="width: 980px;">
<button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
@@ -66,15 +66,16 @@
<div class="nav-collapse collapse pull-right header-menu">
@if(loginAccount.isDefined){
<div class="btn-group" style="margin-top: 0px;">
<a class="dropdown-toggle menu" data-toggle="dropdown" href="#"><i class="octicon octicon-plus" style="font-size: 20px; vertical-align: top; margin-top: 10px;"></i><span class="caret" style="vertical-align: middle;"></span></a>
<ul class="dropdown-menu">
<a class="dropdown-toggle menu" data-toggle="dropdown" href="#"><i class="octicon octicon-plus" style="font-size: 20px; vertical-align: middle;height:20px !important;"></i><span class="caret" style="vertical-align: middle;"></span></a>
<ul class="dropdown-menu pull-right">
<li><a href="@path/new">New repository</a></li>
<li><a href="@path/groups/new">New group</a></li>
</ul>
</div>
<div class="btn-group" style="margin-top: 0px;">
<a class="dropdown-toggle menu" data-toggle="dropdown" href="#" data-toggle="tooltip" data-placement="bottom" title="Signed is as @loginAccount.get.userName">@avatar(loginAccount.get.userName, 16)<span class="caret" style="vertical-align: middle;"></span></a>
<ul class="dropdown-menu">
<ul class="dropdown-menu pull-right">
<li><a href="@url(loginAccount.get.userName)">Your profile</a></li>
<li><a href="@url(loginAccount.get.userName)/_edit">Account settings</a></li>
@if(loginAccount.get.isAdmin){
<li><a href="@path/admin/users">System administration</a></li>

View File

@@ -11,7 +11,6 @@
@sidemenu(path: String, name: String, icon: String, label: String, count: Int = 0) = {
<li @if(active == name){class="active"} @if(!expand){data-toggle="tooltip" data-placement="left" data-original-title="@label"}>
<div class="@if(active == name){margin} else {gradient} pull-left"></div>
<a href="@url(repository)@path">
<i class="menu-icon @if(active == name){menu-icon-active} octicon octicon-@{icon} "></i>
@if(expand){ @label}
@@ -40,7 +39,7 @@
<span class="add-on count"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
</div>
@if(loginAccount.isDefined && isNoGroup){
<form id="fork-form" method="post" action="@path/@repository.owner/@repository.name/fork">
<form id="fork-form" method="post" action="@path/@repository.owner/@repository.name/fork" style="display: none;">
<input type="hidden" name="account" value="@loginAccount.get.userName"/>
</form>
}
@@ -86,21 +85,28 @@
</div>
}
@id.map { id =>
@if(context.platform != "linux" && context.platform != null){
<div style="margin-top: 10px;">
<a href="@repository.httpOpenRepoUrl(context.platform)" id="repository-clone-url" class="btn btn-small" style="width: 147px;font-weight: bold;"><i class="octicon octicon-desktop-download"></i>&nbsp;&nbsp;Clone in Desktop</a>
</div>
}
<div style="margin-top: 10px;">
<a href="@{url(repository)}/archive/@{encodeRefName(id)}.zip" class="btn btn-small" style="width: 147px;font-weight: bold;"><i class="octicon octicon-cloud-download"></i>Download ZIP</a>
</div>
@*
<div style="margin-top: 10px;">
<a href="@{url(repository)}/archive/@{encodeRefName(id)}.tar.gz" class="btn btn-small" style="width: 147px;font-weight: bold;"><i class="octicon octicon-cloud-download"></i>Download TAR.GZ</a>
</div>
*@
}
}
</div>
<div style="margin-right: @if(expand){180px} else {50px};">
@if(expand){
@repository.repository.description.map { description =>
<p class="description">@description</p>
<p class="description">@detectAndRenderLinks(description)</p>
}
<div style="border: 1px solid #eee; padding: 4px; margin-bottom: 10px;">
<div style="margin-bottom: 10px;" class="box-content">
<table class="fill-width">
<tr>
<td style="width: 33%; text-align: center;">
@@ -171,12 +177,14 @@ $(function(){
$('#repository-url-http').click(function(){
$('#repository-url-proto').text('HTTP');
$('#repository-url').val('@repository.httpUrl');
$('#repository-clone-url').attr('href', '@repository.httpOpenRepoUrl(context.platform)')
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
});
$('#repository-url-ssh').click(function(){
$('#repository-url-proto').text('SSH');
$('#repository-url').val('@repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)');
$('#repository-clone-url').attr('href', '@repository.sshOpenRepoUrl(context.platform, settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)');
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
});
}

View File

@@ -4,32 +4,42 @@
@import context._
@import gitbucket.core.view.helpers._
@import gitbucket.core.model._
<div class="box">
<table class="table table-file-list" style="border: 1px solid silver;">
@commits.map { day =>
<tr>
<th colspan="4" class="box-header" style="font-weight: normal;">@date(day.head.commitTime)</th>
</tr>
@day.map { commit =>
<tr>
<td style="width: 20%;">
@avatar(commit, 20)
@user(commit.authorName, commit.authorEmailAddress, "username")
</td>
<td>@commit.shortMessage</td>
<td style="width: 10%; text-align: right">
<span class="badge" style="display: inline">@if(comments.isDefined){
@comments.get.flatMap @{
case comment: CommitComment => Some(comment)
case other => None
}.count(t => t.commitId == commit.id && !t.pullRequest)
}</span>
</td>
<td style="width: 10%; text-align: right;">
<a href="@url(repository)/commit/@commit.id" class="monospace">@commit.id.substring(0, 7)</a>
</td>
</tr>
}
<div class="commit-list">
@commits.map { day =>
<div class="muted" style="background-color: white;">
<i class="octicon octicon-git-commit"></i> Commits on @date(day.head.commitTime)
</div>
<div class="box-commits">
@day.map { commit =>
<div class="box-content-row" style="padding: 8px;">
<ul class="nav nav-pills-group pull-right" style="margin-top: 2px; margin-bottom: 0px; margin-right: 4px;">
<li class="first"><a href="@url(repository)/commit/@commit.id" class="link monospace">@commit.id.substring(0, 7)</a></li>
<li class="last"><a href="@url(repository)/tree/@commit.id" style="padding-top: 9px; padding-bottom: 10px;"><i class="octicon octicon-code link"></i></a></li>
</ul>
<div>
<div class="commit-avatar-image">@avatar(commit, 40)</div>
<div class="commit-message-box">
<a href="@url(repository)/commit/@commit.id" class="commit-message" style="font-weight: bold;">@link(commit.summary, repository)</a>
@if(commit.description.isDefined){
<a href="javascript:void(0)" onclick="$('#description-@commit.id').toggle();" class="omit">...</a>
}
<br>
@if(commit.description.isDefined){
<pre id="description-@commit.id" style="display: none;" class="commit-description">@link(commit.description.get, repository)</pre>
}
<div class="small">
@user(commit.authorName, commit.authorEmailAddress, "username")
<span class="muted">authored @helper.html.datetimeago(commit.authorTime)</span>
@if(commit.isDifferentFromAuthor) {
<span class="octicon octicon-arrow-right" style="margin-top : -2px;"></span>
@user(commit.committerName, commit.committerEmailAddress, "username")
<span class="muted">committed @helper.html.datetimeago(commit.authorTime)</span>
}
</div>
</div>
</div>
</div>
}
</table>
</div>
}
</div>

View File

@@ -9,18 +9,16 @@
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
originRepository: gitbucket.core.service.RepositoryService.RepositoryInfo,
forkedRepository: gitbucket.core.service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context)
hasOriginWritePermission: Boolean,
collaborators: List[String],
milestones: List[gitbucket.core.model.Milestone],
labels: List[gitbucket.core.model.Label])(implicit context: gitbucket.core.controller.Context)
@import context._
@import gitbucket.core.view.helpers._
@html.main(s"Pull Requests - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("pulls", repository){
<div class="pullreq-info">
<div id="compare-info">
<a href="#" id="edit-compare-condition" class="btn btn-mini pull-right">Edit</a>
<span class="label label-info monospace">@originRepository.owner:@originId</span> ... <span class="label label-info monospace">@forkedRepository.owner:@forkedId</span>
</div>
<div id="compare-edit" style="display: none;">
<a href="#" id="cancel-condition-editing" class="pull-right"><i class="icon-remove-circle"></i></a>
<div id="compare-edit">
@helper.html.dropdown(originRepository.owner + "/" + originRepository.name, "base fork") {
@members.map { case (owner, name) =>
<li><a href="#" class="origin-owner" data-owner="@owner" data-name="@name">@helper.html.checkicon(owner == originRepository.owner) @owner/@name</a></li>
@@ -43,41 +41,52 @@
}
}
</div>
</div>
@if(commits.nonEmpty && hasWritePermission){
<div style="margin-bottom: 10px;" id="create-pull-request">
<a href="#" class="btn btn-success" id="show-form">Create pull request</a>
<div class="check-conflict" style="display: none;">
<img src="@assets/common/images/indicator.gif"/> Checking...
</div>
<div id="pull-request-form" class="box" style="display: none;">
<div class="box-content">
<form method="POST" action="@path/@originRepository.owner/@originRepository.name/pulls/new" validate="true">
<div style="width: 240px; position: absolute; margin-left: 610px;">
<div class="check-conflict" style="display: none;">
<img src="@assets/common/images/indicator.gif"/> Checking...
</div>
@if(commits.nonEmpty && loginAccount.isDefined){
<div style="margin-bottom: 10px; padding: 8px; background-color: #fff9ea" id="create-pull-request" class="box-content">
<a href="#" class="btn btn-success" id="show-form">Create pull request</a>
&nbsp;&nbsp;
<span class="muted">Discuss and review the changes in this comparison with others.</span>
</div>
<div id="pull-request-form" @*class="box"*@ style="display: none; margin-bottom: 20px;">
<form method="POST" action="@path/@originRepository.owner/@originRepository.name/pulls/new" validate="true">
<div class="row-fluid">
<div class="span10">
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
<div class="box issue-box">
<div class="box-content">
<span class="error" id="error-title"></span>
<input type="text" name="title" style="width: 690px" placeholder="Title"/>
@helper.html.preview(
repository = repository,
content = "",
enableWikiLink = false,
enableRefsLink = true,
enableTaskList = true,
hasWritePermission = true,
style = "width: 690px; height: 200px;"
)
<input type="hidden" name="targetUserName" value="@originRepository.owner"/>
<input type="hidden" name="targetBranch" value="@originId"/>
<input type="hidden" name="requestUserName" value="@forkedRepository.owner"/>
<input type="hidden" name="requestRepositoryName" value="@forkedRepository.name"/>
<input type="hidden" name="requestBranch" value="@forkedId"/>
<input type="hidden" name="commitIdFrom" value="@sourceId"/>
<input type="hidden" name="commitIdTo" value="@commitId"/>
<div class="align-right">
<input type="submit" class="btn btn-success" value="Create pull request"/>
</div>
</div>
</div>
<div style="width: 600px; border-right: 1px solid #d4d4d4;">
<span class="error" id="error-title"></span>
<input type="text" name="title" style="width: 580px" placeholder="Title"/>
@helper.html.preview(
repository = repository,
content = "",
enableWikiLink = false,
enableRefsLink = true,
enableTaskList = true,
hasWritePermission = hasWritePermission,
style = "width: 580px; height: 200px;"
)
<input type="hidden" name="targetUserName" value="@originRepository.owner"/>
<input type="hidden" name="targetBranch" value="@originId"/>
<input type="hidden" name="requestUserName" value="@forkedRepository.owner"/>
<input type="hidden" name="requestRepositoryName" value="@forkedRepository.name"/>
<input type="hidden" name="requestBranch" value="@forkedId"/>
<input type="hidden" name="commitIdFrom" value="@sourceId"/>
<input type="hidden" name="commitIdTo" value="@commitId"/>
</div>
</form>
</div>
<div class="span2">
@issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map((_, 0, 0)), labels, hasOriginWritePermission, repository)
</div>
</div>
</form>
</div>
}
@if(commits.isEmpty){
@@ -90,72 +99,119 @@
</tr>
</table>
} else {
@pulls.html.commits(commits, Some(comments), repository)
@helper.html.diff(diffs, repository, Some(commitId), Some(sourceId), true, None, hasWritePermission, false)
<div style="margin-bottom: 10px;" class="box-content">
<table class="fill-width">
<tr>
<td style="width: 25%; text-align: center;">
<i class="octicon octicon-commit"></i>
@defining(commits.flatten){ commits =>
<strong>@commits.size</strong> @plural(commits.size, "commit")
}
</td>
<td style="width: 25%; text-align: center;">
<i class="octicon octicon-diff"></i>
<strong>@diffs.size</strong> @plural(diffs.size, "file") changed
</td>
<td style="width: 25%; text-align: center;">
<i class="octicon octicon-comment"></i>
@defining(comments.collect { case c: gitbucket.core.model.CommitComment => c }){ comments =>
<strong>@comments.size</strong> commit @plural(comments.size, "comment")
}
</td>
<td style="width: 25%; text-align: center;">
<i class="octicon octicon-organization"></i>
@defining(commits.flatMap(_.map(_.authorEmailAddress)).distinct){ contributors =>
<strong>@contributors.size</strong> @plural(contributors.size, "contributor")
}
</td>
</tr>
</table>
</div>
<div class="box" style="margin-bottom: 20px;">
@commits.map { day =>
<div style="margin-top: 8px; margin-bottom: 8px;" class="muted">
Commits on @date(day.head.commitTime)
</div>
<table style="width: 100%;">
@day.map { commit =>
<tr>
<td style="width: 20%;">
<i class="octicon octicon-git-commit"></i>
@avatar(commit, 20)
@user(commit.authorName, commit.authorEmailAddress, "username strong")
</td>
<td><span class="monospace">@commit.shortMessage</span></td>
@*
<span class="badge" style="display: inline">@if(comments.isDefined){
@comments.get.flatMap @{
case comment: CommitComment => Some(comment)
case other => None
}.count(t => t.commitId == commit.id && !t.pullRequest)
}</span>
*@
<td style="width: 10%; text-align: right;">
<a href="@url(repository)/commit/@commit.id" class="monospace">@commit.id.substring(0, 7)</a>
</td>
</tr>
}
</table>
}
</div>
@helper.html.diff(diffs, repository, Some(commitId), Some(sourceId), true, None, false, false)
<p>Showing you all comments on commits in this comparison.</p>
@issues.html.commentlist(None, comments, hasWritePermission, repository, None)
@issues.html.commentlist(None, comments, false, repository, None)
}
}
}
<script>
$(function(){
$('#edit-compare-condition').click(function(){
$('#compare-info').hide();
$('#compare-edit').show();
});
$('#cancel-condition-editing').click(function(){
$('#compare-info').show();
$('#compare-edit').hide();
});
$('a.origin-owner, a.forked-owner, a.origin-branch, a.forked-branch').click(function(){
var e = $(this);
e.parents('ul').find('i').attr('class', 'icon-white');
e.find('i').attr('class', 'icon-ok');
e.find('i').attr('class', 'octicon-check');
e.parents('div.btn-group').find('button span.strong').text(e.text());
@if(members.isEmpty){
location.href = '@url(repository)/compare/' +
$.trim($('i.icon-ok').parents('a.origin-branch').data('branch')) + '...' +
$.trim($('i.icon-ok').parents('a.forked-branch').data('branch'));
$.trim($('i.octicon-check').parents('a.origin-branch').data('branch')) + '...' +
$.trim($('i.octicon-check').parents('a.forked-branch').data('branch'));
} else {
location.href = '@path/' +
$.trim($('i.icon-ok').parents('a.forked-owner' ).data('owner')) + '/' +
$.trim($('i.icon-ok').parents('a.forked-owner' ).data('name')) +'/compare/' +
$.trim($('i.icon-ok').parents('a.origin-owner' ).data('owner')) + ':' +
$.trim($('i.icon-ok').parents('a.origin-branch').data('branch')) + '...' +
$.trim($('i.icon-ok').parents('a.forked-owner' ).data('owner')) + ':' +
$.trim($('i.icon-ok').parents('a.forked-branch').data('branch'));
$.trim($('i.octicon-check').parents('a.forked-owner' ).data('owner')) + '/' +
$.trim($('i.octicon-check').parents('a.forked-owner' ).data('name')) +'/compare/' +
$.trim($('i.octicon-check').parents('a.origin-owner' ).data('owner')) + ':' +
$.trim($('i.octicon-check').parents('a.origin-branch').data('branch')) + '...' +
$.trim($('i.octicon-check').parents('a.forked-owner' ).data('owner')) + ':' +
$.trim($('i.octicon-check').parents('a.forked-branch').data('branch'));
}
});
$('#show-form').click(function(){
$(this).hide();
$('#create-pull-request').hide();
$('#pull-request-form').show();
});
if(location.search.substr(1).split("&").indexOf("expand=1")!=-1){
$('#show-form').click();
}
@if(hasWritePermission){
@if(loginAccount.isDefined){
function checkConflict(from, to){
$('.check-conflict').show();
$.get('@url(repository)/compare/' + from + '...' + to + '/mergecheck',
$.get('@url(forkedRepository)/compare/' + from + '...' + to + '/mergecheck',
function(data){ $('.check-conflict').html(data); });
}
@if(members.isEmpty){
checkConflict(
$.trim($('i.icon-ok').parents('a.origin-branch').data('branch')),
$.trim($('i.icon-ok').parents('a.forked-branch').data('branch'))
$.trim($('i.octicon-check').parents('a.origin-branch').data('branch')),
$.trim($('i.octicon-check').parents('a.forked-branch').data('branch'))
);
} else {
checkConflict(
$.trim($('i.icon-ok').parents('a.origin-owner' ).data('owner')) + ":" +
$.trim($('i.icon-ok').parents('a.origin-branch').data('branch')),
$.trim($('i.icon-ok').parents('a.forked-owner' ).data('owner')) + ":" +
$.trim($('i.icon-ok').parents('a.forked-branch').data('branch'))
$.trim($('i.octicon-check').parents('a.origin-owner' ).data('owner')) + ":" +
$.trim($('i.octicon-check').parents('a.origin-branch').data('branch')),
$.trim($('i.octicon-check').parents('a.forked-owner' ).data('owner')) + ":" +
$.trim($('i.octicon-check').parents('a.forked-branch').data('branch'))
);
}
}

View File

@@ -45,7 +45,7 @@
}
</div>
<div class="span2">
@issues.html.issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
@issues.html.issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
</div>
</div>
<script>

View File

@@ -1,9 +1,12 @@
@(hasConflict: Boolean)
@if(hasConflict){
<h4>We cant automatically merge these branches</h4>
<p>Don't worry, you can still submit the pull request.</p>
} else {
<h4 style="color: #468847;">Able to merge</h4>
<p>These branches can be automatically merged.</p>
}
<input type="submit" class="btn btn-success btn-block" value="Create pull request"/>
<div style="margin-top: 8px;">
@if(hasConflict){
<i class="octicon octicon-x" style="color: #bd2c00"></i>
<span class="strong" style="color: #bd2c00;">Cant automatically merge.</span>
<span class="muted">Dont worry, you can still create the pull request.</span>
} else {
<i class="octicon octicon-check" style="color: #6cc644"></i>
<span class="strong" style="color: #6cc644">Able to merge.</span>
<span class="muted">These branches can be automatically merged.</span>
}
</div>

View File

@@ -3,8 +3,9 @@
issue: gitbucket.core.model.Issue,
pullreq: gitbucket.core.model.PullRequest,
statuses: List[model.CommitStatus],
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
requestRepositoryUrl: String)(implicit context: gitbucket.core.controller.Context)
originRepository: gitbucket.core.service.RepositoryService.RepositoryInfo,
forkedRepository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.service.SystemSettingsService
@import context._
@import gitbucket.core.view.helpers._
@import model.CommitState
@@ -22,100 +23,101 @@
@status.description.map{ desc => <span class="muted">— @desc</span> }
</div>
}
}else{
@defining(statuses.groupBy(_.state)){ stateMap => @defining(CommitState.combine(stateMap.keySet)){ state =>
<div class="build-status-item">
<a class="pull-right" id="toggle-all-checks"></a>
<span class="build-status-icon text-@{state.name}">@commitStateIcon(state)</span>
<strong class="text-@{state.name}">@commitStateText(state, pullreq.commitIdTo)</strong>
<span class="text-@{state.name}">— @{stateMap.map{ case (keyState, states) => states.size+" "+keyState.name }.mkString(", ")} checks</span>
</div>
<div class="build-statuses-list" style="@if(state==CommitState.SUCCESS){ display:none; }else{ }">
@statuses.map{ status =>
} else {
@defining(statuses.groupBy(_.state)){ stateMap =>
@defining(CommitState.combine(stateMap.keySet)){ state =>
<div class="build-status-item">
@status.targetUrl.map{ url => <a class="pull-right" href="@url">Details</a> }
<span class="build-status-icon text-@{status.state.name}">@commitStateIcon(status.state)</span>
<span class="text-@{status.state.name}">@status.context</span>
@status.description.map{ desc => <span class="muted">— @desc</span> }
<a class="pull-right" id="toggle-all-checks"></a>
<span class="build-status-icon text-@{state.name}">@commitStateIcon(state)</span>
<strong class="text-@{state.name}">@commitStateText(state, pullreq.commitIdTo)</strong>
<span class="text-@{state.name}">— @{stateMap.map{ case (keyState, states) => states.size+" "+keyState.name }.mkString(", ")} checks</span>
</div>
}
</div>
} }
<div class="build-statuses-list" style="@if(state==CommitState.SUCCESS){ display:none; }else{ }">
@statuses.map{ status =>
<div class="build-status-item">
@status.targetUrl.map{ url => <a class="pull-right" href="@url">Details</a> }
<span class="build-status-icon text-@{status.state.name}">@commitStateIcon(status.state)</span>
<span class="text-@{status.state.name}">@status.context</span>
@status.description.map{ desc => <span class="muted">— @desc</span> }
</div>
}
</div>
}
}
}
</div>
}
<div class="pull-right">
<input type="button" class="btn @if(!hasProblem){ btn-success }" id="merge-pull-request-button" value="Merge pull request"@if(hasConflict){ disabled="true"}/>
</div>
<div>
@if(hasConflict){
<span class="strong">We cant automatically merge this pull request.</span>
} else{ @if(hasProblem){
<span class="strong">Merge with caution!</span>
} else {
<span class="strong">This pull request can be automatically merged.</span>
} }
</div>
<div class="small">
@if(hasConflict){
<a href="#" id="show-command-line">Use the command line</a> to resolve conflicts before continuing.
} else {
You can also merge branches on the <a href="#" id="show-command-line">command line</a>.
}
</div>
<div id="command-line" style="display: none;">
<hr>
@if(hasConflict){
<span class="strong">Checkout via command line</span>
<p>
If you cannot merge a pull request automatically here, you have the option of checking
it out via command line to resolve conflicts and perform a manual merge.
</p>
} else {
<span class="strong">Merging via command line</span>
<p>
If you do not want to use the merge button or an automatic merge cannot be performed,
you can perform a manual merge on the command line.
</p>
}
@helper.html.copy("repository-url-copy", requestRepositoryUrl){
<input type="text" style="width: 500px;" value="@requestRepositoryUrl" id="repository-url" readonly>
}
<div>
<p>
<span class="strong">Step 1:</span> Check out a new branch to test the changes — run this from your project directory
</p>
@defining(s"git checkout -b ${pullreq.requestUserName}-${pullreq.requestBranch} ${pullreq.branch}"){ command =>
@helper.html.copy("merge-command-copy-1", command){
<pre style="width: 500px; float: left;">@command</pre>
}
}
</div>
<div>
<p>
<span class="strong">Step 2:</span> Bring in @{pullreq.requestUserName}'s changes and test
</p>
@defining(s"git pull ${requestRepositoryUrl} ${pullreq.requestBranch}"){ command =>
@helper.html.copy("merge-command-copy-2", command){
<pre style="width: 500px; float: left;">@command</pre>
}
}
</div>
<div>
<p>
<span class="strong">Step 3:</span> Merge the changes and update the server
</p>
@defining(s"git checkout ${pullreq.branch}\ngit merge ${pullreq.requestUserName}-${pullreq.requestBranch}\ngit push origin ${pullreq.branch}"){ command =>
@helper.html.copy("merge-command-copy-3", command){
<pre style="width: 500px; float: left;">@command</pre>
}
}
</div>
</div>
<div class="pull-right">
<input type="button" class="btn @if(!hasProblem){ btn-success }" id="merge-pull-request-button" value="Merge pull request"@if(hasConflict){ disabled="true"}/>
</div>
<div>
@if(hasConflict){
<span class="strong">We cant automatically merge this pull request.</span>
} else {
@if(hasProblem){
<span class="strong">Merge with caution!</span>
} else {
<span class="strong">This pull request can be automatically merged.</span>
}
}
</div>
<div class="small">
@if(hasConflict){
<a href="#" id="show-command-line">Use the command line</a> to resolve conflicts before continuing.
} else {
You can also merge branches on the <a href="#" id="show-command-line">command line</a>.
}
</div>
<div id="command-line" style="display: none;">
<hr>
@if(hasConflict){
<span class="strong">Checkout via command line</span>
<p>
If you cannot merge a pull request automatically here, you have the option of checking
it out via command line to resolve conflicts and perform a manual merge.
</p>
} else {
<span class="strong">Merging via command line</span>
<p>
If you do not want to use the merge button or an automatic merge cannot be performed,
you can perform a manual merge on the command line.
</p>
}
@helper.html.copy("repository-url-copy", forkedRepository.httpUrl, true){
<div class="btn-group" data-toggle="buttons-radio">
<button class="btn btn-small active" type="button" id="repository-url-http">HTTP</button>
@if(settings.ssh && loginAccount.isDefined){
<button class="btn btn-small" type="button" id="repository-url-ssh" style="border-radius: 0px;">SSH</button>
}
</div>
<input type="text" style="width: 500px;" value="@forkedRepository.httpUrl" id="repository-url" readonly>
}
<div>
<p>
<span class="strong">Step 1:</span> From your project repository, check out a new branch and test the changes.
</p>
@defining(s"git checkout -b ${pullreq.requestUserName}-${pullreq.requestBranch} ${pullreq.branch}\n" +
s"git pull ${forkedRepository.httpUrl} ${pullreq.requestBranch}"){ command =>
@helper.html.copy("merge-command-copy-1", command){
<pre style="width: 600px; float: left; font-size: 12px; border-radius: 3px 0px 3px 3px;" id="merge-command">@Html(command)</pre>
}
}
</div>
<div>
<p>
<span class="strong">Step 2:</span> Merge the changes and update on the server.
</p>
@defining(s"git checkout ${pullreq.branch}\ngit merge --no-ff ${pullreq.requestUserName}-${pullreq.requestBranch}\n" +
s"git push origin ${pullreq.branch}"){ command =>
@helper.html.copy("merge-command-copy-2", command){
<pre style="width: 600px; float: left; font-size: 12px; border-radius: 3px 0px 3px 3px;">@command</pre>
}
}
</div>
</div>
</div>
<div id="confirm-merge-form" style="display: none;">
<form method="POST" action="@url(repository)/pull/@pullreq.issueId/merge">
<form method="POST" action="@url(originRepository)/pull/@pullreq.issueId/merge">
<div class="strong">
Merge pull request #@issue.issueId from @{pullreq.requestUserName}/@{pullreq.requestBranch}
</div>
@@ -149,5 +151,31 @@ $(function(){
$('#merge-pull-request').hide();
$('#confirm-merge-form').show();
});
@if(settings.ssh && loginAccount.isDefined){
$('#repository-url-http').click(function(){
// Update URL box
$('#repository-url').val('@forkedRepository.httpUrl');
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
// Update command guidance
$('#merge-command').text($('#merge-command').text().replace(
'@forkedRepository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)',
'@forkedRepository.httpUrl'
));
$('#merge-command-copy-1').attr('data-clipboard-text', $('#merge-command').text());
});
$('#repository-url-ssh').click(function(){
// Update URL box
$('#repository-url').val('@forkedRepository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)');
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
// Update command guidance
$('#merge-command').text($('#merge-command').text().replace(
'@forkedRepository.httpUrl',
'@forkedRepository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)'
));
$('#merge-command-copy-1').attr('data-clipboard-text', $('#merge-command').text());
});
}
});
</script>

View File

@@ -18,9 +18,9 @@
<div>
<div class="show-title pull-right">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a class="btn btn-small" href="#" id="edit">Edit</a>
<a class="btn" href="#" id="edit">Edit</a>
}
<a class="btn btn-small btn-success" href="@url(repository)/issues/new">New issue</a>
<a class="btn btn-success" href="@url(repository)/issues/new">New issue</a>
</div>
<div class="edit-title pull-right" style="display: none;">
<a class="btn" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a>

View File

@@ -11,7 +11,7 @@
@html.menu("code", repository){
<div class="head">
<div class="pull-right hide-if-blame"><div class="btn-group">
<a href="@url(repository)/find/@encodeRefName(branch)" class="btn btn-mini" data-toggle="tooltip" data-placement="bottom" data-hotkey="t" title="Quickly jump between files"><i class="icon icon-th-list"></i></a>
<a href="@url(repository)/find/@encodeRefName(branch)" class="btn btn-mini" data-toggle="tooltip" data-placement="bottom" data-hotkey="t" title="Quickly jump between files"><i class="octicon octicon-list-unordered"></i></a>
</div></div>
<div class="line-age-legend">
<span>Newer</span>
@@ -47,57 +47,50 @@
}
}
</div>
<table class="table table-bordered blobview">
<tr>
<th style="font-weight: normal;">
<div class="pull-left">
@avatar(latestCommit, 20)
@user(latestCommit.authorName, latestCommit.authorEmailAddress, "username strong")
<span class="muted">@helper.html.datetimeago(latestCommit.commitTime)</span>
<a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a>
</div>
<div class="btn-group pull-right">
@if(hasWritePermission && content.viewType == "text" && repository.branchList.contains(branch)){
<a class="btn btn-mini" href="@url(repository)/edit/@encodeRefName(branch)/@pathList.mkString("/")">Edit</a>
}
<a class="btn btn-mini" href="?raw=true">Raw</a>
@if(content.viewType == "text"){
<a class="btn btn-mini blame-action" href="@url(repository)/blame/@latestCommit.id/@pathList.mkString("/")" data-url="@url(repository)/get-blame/@latestCommit.id/@pathList.mkString("/")" data-repository="@url(repository)">Blame</a>
}
<a class="btn btn-mini" href="@url(repository)/commits/@encodeRefName(branch)/@pathList.mkString("/")">History</a>
@if(hasWritePermission){
<a class="btn btn-mini btn-danger" href="@url(repository)/remove/@encodeRefName(branch)/@pathList.mkString("/")">Delete</a>
}
</div>
</th>
</tr>
<tr>
<td>
@if(content.viewType == "text"){
@defining(isRenderable(pathList.reverse.head)){ isRrenderable =>
@if(!isBlame && isRrenderable) {
<div class="box-content markdown-body" style="border: none; padding-left: 16px; padding-right: 16px;">
@renderMarkup(pathList, content.content.get, branch, repository, false, false, true)
</div>
} else {
<pre class="prettyprint linenums blob @if(!isRrenderable){ no-renderable } ">@content.content.get</pre>
}
}
}
@if(content.viewType == "image"){
<img src="?raw=true"/>
}
@if(content.viewType == "large" || content.viewType == "binary"){
<div style="text-align: center; padding-top: 20px; padding-bottom: 20px;">
<a href="?raw=true">View Raw</a><br>
<br>
(Sorry about that, but we can't show files that are this big right now)
<div class="box-header">
@avatar(latestCommit, 20)
@user(latestCommit.authorName, latestCommit.authorEmailAddress, "username strong")
<span class="muted">@helper.html.datetimeago(latestCommit.commitTime)</span>
<a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a>
<div class="btn-group pull-right">
@if(hasWritePermission && content.viewType == "text" && repository.branchList.contains(branch)){
<a class="btn btn-mini" href="@url(repository)/edit/@encodeRefName(branch)/@pathList.mkString("/")">Edit</a>
}
<a class="btn btn-mini" href="?raw=true">Raw</a>
@if(content.viewType == "text"){
<a class="btn btn-mini blame-action" href="@url(repository)/blame/@latestCommit.id/@pathList.mkString("/")" data-url="@url(repository)/get-blame/@latestCommit.id/@pathList.mkString("/")" data-repository="@url(repository)">Blame</a>
}
<a class="btn btn-mini" href="@url(repository)/commits/@encodeRefName(branch)/@pathList.mkString("/")">History</a>
@if(hasWritePermission && repository.branchList.contains(branch)){
<a class="btn btn-mini btn-danger" href="@url(repository)/remove/@encodeRefName(branch)/@pathList.mkString("/")">Delete</a>
}
</div>
</div>
@if(content.viewType == "text"){
@defining(isRenderable(pathList.reverse.head)){ isRrenderable =>
@if(!isBlame && isRrenderable) {
<div class="box-content-bottom markdown-body" style="padding-left: 16px; padding-right: 16px;">
@renderMarkup(pathList, content.content.get, branch, repository, false, false, true)
</div>
} else {
<div class="box-content-bottom">
<pre class="prettyprint linenums blob @if(!isRrenderable){ no-renderable } ">@content.content.get</pre>
</div>
}
</td>
</tr>
</table>
}
}
@if(content.viewType == "image"){
<div class="box-content-bottom">
<img src="?raw=true"/>
</div>
}
@if(content.viewType == "large" || content.viewType == "binary"){
<div class="box-content-bottom" style="text-align: center; padding-top: 20px; padding-bottom: 20px;">
<a href="?raw=true">View Raw</a><br>
<br>
(Sorry about that, but we can't show files that are this big right now)
</div>
}
}
}
<script src="@assets/vendors/jquery/jquery.ba-hashchange.js"></script>
@@ -111,36 +104,38 @@ $(window).load(function(){
function updateSourceLineNum(){
$('.source-line-num').remove();
var pos = pre.find('ol.linenums').position();
$('<div class="source-line-num">').css({
height:pre.height(),
width:'48px',
cursor:'pointer',
position: 'absolute',
top : pos.top + 'px',
left : pos.left + 'px'
}).click(function(e){
$(window).hashchange(function(){})
var pos = $(this).data("pos");
if(!pos){
pos = $('ol.linenums li').map(function(){ return {id:$(this).attr("id"),top:$(this).position().top} }).toArray();
$(this).data("pos",pos);
}
for(var i=0;i<pos.length-1;i++){
if(pos[i+1].top>e.pageY){
break;
if(pos){
$('<div class="source-line-num">').css({
height : pre.height(),
width : '48px',
cursor : 'pointer',
position: 'absolute',
top : pos.top + 'px',
left : pos.left + 'px'
}).click(function(e){
$(window).hashchange(function(){})
var pos = $(this).data("pos");
if(!pos){
pos = $('ol.linenums li').map(function(){ return { id: $(this).attr("id"), top: $(this).position().top} }).toArray();
$(this).data("pos",pos);
}
}
var line = pos[i].id.replace(/^L/,'');
var hash = location.hash;
if(e.shiftKey == true && hash.match(/#L\d+(-L\d+)?/)){
var lines = hash.split('-');
location.hash = lines[0] + '-L' + line;
} else {
var p = $("#L"+line).attr('id',"");
location.hash = '#L' + line;
p.attr('id','L'+line);
}
}).appendTo(pre);
for(var i = 0; i < pos.length-1; i++){
if(pos[i + 1].top > e.pageY){
break;
}
}
var line = pos[i].id.replace(/^L/,'');
var hash = location.hash;
if(e.shiftKey == true && hash.match(/#L\d+(-L\d+)?/)){
var lines = hash.split('-');
location.hash = lines[0] + '-L' + line;
} else {
var p = $("#L"+line).attr('id',"");
location.hash = '#L' + line;
p.attr('id','L'+line);
}
}).appendTo(pre);
}
}
var repository = $('.blame-action').data('repository');
$('.blame-action').click(function(e){
@@ -152,11 +147,11 @@ $(window).load(function(){
});
function updateBlame(){
var m = /^\/(blame|blob)(\/.*)$/.exec(location.href.substring(repository.length));
var m = /\/(blame|blob)(\/.*)$/.exec(location.href);
var mode = m[1];
$('.blame-action').toggleClass("active", mode=='blame').attr('href', repository + (m[1]=='blame'?'/blob':'/blame')+m[2]);
if(pre.parents("td").find(".blame").length){
pre.parents("div.container").toggleClass("blame-container", mode=='blame');
$('.blame-action').toggleClass("active", mode=='blame').attr('href', repository + (m[1] == 'blame' ? '/blob' : '/blame') + m[2]);
if(pre.parents("div.box-content-bottom").find(".blame").length){
pre.parents("div.container").toggleClass("blame-container", mode == 'blame');
updateSourceLineNum();
return;
}
@@ -164,47 +159,47 @@ $(window).load(function(){
updateSourceLineNum();
return;
}
$(document.body).toggleClass('no-box-shadow',document.body.style.boxShadow===undefined);
$(document.body).toggleClass('no-box-shadow', document.body.style.boxShadow===undefined);
$('.blame-action').addClass("active");
var base = $('<div class="blame">').css({height:pre.height()}).prependTo(pre.parents("td")[0]);
var base = $('<div class="blame">').css({height: pre.height()}).prependTo(pre.parents("div.box-content-bottom"));
base.parents("div.container").addClass("blame-container");
updateSourceLineNum();
$.get($('.blame-action').data('url')).done(function(data){
var blame = data.blame;
var index = [];
for(var i=0;i<blame.length;i++){
for(var j=0;j<blame[i].lines.length;j++){
index[blame[i].lines[j]]=blame[i];
for(var i = 0; i < blame.length; i++){
for(var j = 0; j < blame[i].lines.length; j++){
index[blame[i].lines[j]] = blame[i];
}
}
var blame, lastDiv, now=new Date().getTime();
var blame, lastDiv, now = new Date().getTime();
$('pre.prettyprint ol.linenums li').each(function(i, e){
var p=$(e).position();
var h=$(e).height();
var p = $(e).position();
var h = $(e).height();
if(blame == index[i]){
lastDiv.css("min-height",(p.top+h+1) - lastDiv.position().top);
lastDiv.css("min-height",(p.top + h + 1) - lastDiv.position().top);
}else{
$(e).addClass('blame-sep')
blame = index[i];
var sha = $('<div class="blame-sha">')
.append($('<a>').attr("href",data.root+'/commit/'+blame.id).text(blame.id.substr(0,7)));
.append($('<a>').attr("href", data.root + '/commit/' + blame.id).text(blame.id.substr(0,7)));
if(blame.prev){
sha.append($('<br />'))
.append($('<a class="muted-link">').text('prev').attr("href",data.root+'/blame/'+blame.prev+'/'+(blame.prevPath||data.path)));
.append($('<a class="muted-link">').text('prev').attr("href", data.root + '/blame/' + blame.prev + '/' + (blame.prevPath || data.path)));
}
lastDiv = $('<div class="blame-info">')
.addClass('heat'+Math.min(10,Math.max(1,Math.ceil((now-blame.commited)/(24*3600*1000*70)))))
.toggleClass('blame-last',blame.id==data.last)
.addClass('heat' + Math.min(10, Math.max(1, Math.ceil((now - blame.commited) / (24 * 3600 * 1000 * 70)))))
.toggleClass('blame-last', blame.id == data.last)
.data('line', (i + 1))
.css({
"top" : p.top + 'px',
"min-height" : h+'px'
"top" : p.top + 'px',
"min-height" : h + 'px'
})
.append(sha)
.append($(blame.avatar).addClass('avatar').css({"float":"left"}))
.append($(blame.avatar).addClass('avatar').css({"float": "left"}))
.append($('<div class="blame-commit-title">').text(blame.message))
.append($('<div class="muted">').html(blame.author+ " authed "+blame.authed))
.append($('<div class="muted">').html(blame.author + " authed " + blame.authed))
.appendTo(base);
}
});
@@ -214,6 +209,8 @@ $(window).load(function(){
updateBlame();
});
var scrolling = false;
/**
* Hightlight lines which are specified by URL hash.
*/
@@ -224,7 +221,7 @@ function updateHighlighting(){
var lines = hash.substr(1).split('-');
if(lines.length == 1){
$('#' + lines[0]).addClass('highlight');
if(!updateHighlighting.scrolling){
if(!scrolling){
$(window).scrollTop($('#' + lines[0]).offset().top - 40);
}
} else if(lines.length > 1){
@@ -233,11 +230,11 @@ function updateHighlighting(){
for(var i = start; i <= end; i++){
$('#L' + i).addClass('highlight');
}
if(!updateHighlighting.scrolling){
if(!scrolling){
$(window).scrollTop($('#L' + start).offset().top - 40);
}
}
updateHighlighting.scrolling = true;
scrolling = true;
}
}
</script>

View File

@@ -35,7 +35,7 @@
}.getOrElse {
encodeRefName(repository.repository.defaultBranch)
}}...@{encodeRefName(branch.name)}?expand=1" class="btn btn-small">New Pull Request</a>
}else{
} else {
<a href="@url(repository)/compare/@{repository.repository.parentUserName.map { parent =>
urlEncode(parent) + ":" + encodeRefName(repository.repository.defaultBranch)
}.getOrElse {
@@ -45,9 +45,9 @@
}
@if(hasWritePermission){
@if(prs.map(!_._2.closed).getOrElse(false)){
<a class="btn disabled btn-mini" data-toggle="tooltip" title="You cant delete this branch because it has an open pull request"><i class="icon icon-trash icon-white"></i></a>
<a class="btn disabled btn-mini" data-toggle="tooltip" title="You cant delete this branch because it has an open pull request"><i class="octicon octicon-trashcan"></i></a>
}else{
<a href="@url(repository)/delete/@encodeRefName(branch.name)" class="btn @if(info.isMerged){ btn-warning }else{ btn-danger } delete-branch btn-mini" data-name="@branch.name" @if(info.isMerged){ data-toggle="tooltip" title="this branch is merged" }><i class="icon icon-trash icon-white"></i></a>
<a href="@url(repository)/delete/@encodeRefName(branch.name)" class="btn @if(info.isMerged){ btn-warning }else{ btn-danger } delete-branch btn-mini" data-name="@branch.name" @if(info.isMerged){ data-toggle="tooltip" title="this branch is merged" }><i class="octicon octicon-trashcan"></i></a>
}
}
}

View File

@@ -14,7 +14,7 @@
@html.menu("code", repository){
<table class="table table-bordered">
<tr>
<th>
<th class="box-header">
<div class="pull-right align-right">
<a href="@url(repository)/tree/@commit.id" class="btn btn-small">Browse code</a>
</div>
@@ -74,7 +74,7 @@
</div>
@if(commit.isDifferentFromAuthor) {
<div class="committer">
<span class="icon-arrow-right"></span>
<span class="octicon octicon-arrow-right"></span>
<span>@user(commit.committerName, commit.committerEmailAddress, "username strong")</span>
<span class="muted"> committed @helper.html.datetimeago(commit.commitTime)</span>
</div>

View File

@@ -10,72 +10,81 @@
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.menu("code", repository){
<div class="head">
@helper.html.branchcontrol(
branch,
repository,
hasWritePermission
){
@repository.branchList.map { x =>
<li><a href="@url(repository)/commits/@encodeRefName(x)">@helper.html.checkicon(x == branch) @x</a></li>
}
}
@if(pathList.isEmpty){
<a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> / Commit History
@helper.html.branchcontrol(
branch,
repository,
hasWritePermission
){
@repository.branchList.map { x =>
<li><a href="@url(repository)/commits/@encodeRefName(x)">@helper.html.checkicon(x == branch) @x</a></li>
}
}
}
@if(pathList.nonEmpty){
<span class="muted">History for</span>
<a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> /
<a class="strong" href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> /
@pathList.zipWithIndex.map { case (section, i) =>
@if(i == pathList.length - 1){
@section
<span class="strong">@section</span>
} else {
<a href="@url(repository)/tree/@encodeRefName(branch)/@pathList.take(i + 1).mkString("/")">@section</a> /
<a class="strong" href="@url(repository)/tree/@encodeRefName(branch)/@pathList.take(i + 1).mkString("/")">@section</a> /
}
}
}
</div>
@commits.map { day =>
<table class="table table-bordered">
<tr>
<th>@date(day.head.commitTime)</th>
</tr>
@day.map { commit =>
<tr>
<td>
<div class="pull-right align-right">
<a href="@url(repository)/commit/@commit.id" class="btn btn-small monospace">@commit.id.substring(0, 10)</a><br>
<a href="@url(repository)/tree/@commit.id" class="small">Browse code</a>
</div>
<div>
<div class="commit-avatar-image">@avatar(commit, 40)</div>
<div class="commit-message-box">
<a href="@url(repository)/commit/@commit.id" class="commit-message" style="font-weight: bold;">@link(commit.summary, repository)</a>
@if(commit.description.isDefined){
<a href="javascript:void(0)" onclick="$('#description-@commit.id').toggle();" class="omit">...</a>
}
<br>
@if(commit.description.isDefined){
<pre id="description-@commit.id" style="display: none;" class="commit-description">@link(commit.description.get, repository)</pre>
}
<div class="small">
@user(commit.authorName, commit.authorEmailAddress, "username")
<span class="muted">authored @helper.html.datetimeago(commit.authorTime)</span>
@if(commit.isDifferentFromAuthor) {
<span class="icon-arrow-right" style="margin-top : -2px ;"></span>
@user(commit.committerName, commit.committerEmailAddress, "username")
<span class="muted">committed @helper.html.datetimeago(commit.authorTime)</span>
<div class="commit-list">
@commits.map { day =>
<div class="muted" style="background-color: white;">
<i class="octicon octicon-git-commit"></i> Commits on @date(day.head.commitTime)
</div>
<div class="box-commits">
@day.map { commit =>
<div class="box-content-row" style="padding: 8px;">
<ul class="nav nav-pills-group pull-right" style="margin-top: 2px; margin-bottom: 0px; margin-right: 4px;">
<li class="first"><a href="@url(repository)/commit/@commit.id" class="link monospace">@commit.id.substring(0, 7)</a></li>
<li class="last"><a href="@url(repository)/tree/@commit.id" style="padding-top: 9px; padding-bottom: 10px;"><i class="octicon octicon-code link"></i></a></li>
</ul>
<div>
<div class="commit-avatar-image">@avatar(commit, 40)</div>
<div class="commit-message-box">
<a href="@url(repository)/commit/@commit.id" class="commit-message" style="font-weight: bold;">@link(commit.summary, repository)</a>
@if(commit.description.isDefined){
<a href="javascript:void(0)" onclick="$('#description-@commit.id').toggle();" class="omit">...</a>
}
<br>
@if(commit.description.isDefined){
<pre id="description-@commit.id" style="display: none;" class="commit-description">@link(commit.description.get, repository)</pre>
}
<div class="small">
@user(commit.authorName, commit.authorEmailAddress, "username")
<span class="muted">authored @helper.html.datetimeago(commit.authorTime)</span>
@if(commit.isDifferentFromAuthor) {
<span class="octicon octicon-arrow-right" style="margin-top : -2px;"></span>
@user(commit.committerName, commit.committerEmailAddress, "username")
<span class="muted">committed @helper.html.datetimeago(commit.authorTime)</span>
}
</div>
</div>
</div>
</div>
</td>
</tr>
}
</div>
}
</div>
<div class="pagination" style="text-align: center; margin-top: 30px">
<ul>
@if(page <= 1){
<li class="disabled"><span>Newer</span></li>
} else {
<li><a href="?page=@{page - 1}">Newer</a></li>
}
</table>
}
<div class="btn-group">
<button class="btn" onclick="location.href='?page=@{page - 1}'"@if(page <= 1){ disabled="true"}>&lt; Newer</button>
<button class="btn" onclick="location.href='?page=@{page + 1}'"@if(!hasNext){ disabled="true"}>Older &gt;</button>
@if(!hasNext){
<li class="disabled"><span>Older</span></li>
} else {
<li><a href="?page=@{page + 1}">Older</a></li>
}
</ul>
</div>
}
}

View File

@@ -15,15 +15,15 @@
@html.menu("code", repository, Some(branch), pathList.isEmpty, groupNames.isEmpty, info, error){
<div class="head">
<div class="pull-right"><div class="btn-group">
<a href="@url(repository)/find/@encodeRefName(branch)" class="btn btn-mini" data-toggle="tooltip" data-placement="bottom" data-hotkey="t" title="Quickly jump between files"><i class="icon icon-th-list"></i></a>
<a href="@url(repository)/find/@encodeRefName(branch)" class="btn btn-small" data-toggle="tooltip" data-placement="bottom" data-hotkey="t" title="Quickly jump between files"><i class="octicon octicon-list-unordered"></i></a>
@if(pathList.nonEmpty){
<a href="@url(repository)/commits/@encodeRefName(branch)/@pathList.mkString("/")" class="btn btn-mini" data-toggle="tooltip" data-placement="bottom" title="Browse commits for this branch"><i class="icon icon-time"></i></a>
<a href="@url(repository)/commits/@encodeRefName(branch)/@pathList.mkString("/")" class="btn btn-small" data-toggle="tooltip" data-placement="bottom" title="Browse commits for this branch"><i class="octicon octicon-clock"></i></a>
}
</div></div>
@branchPullRequest.map{ case (pullRequest, issue) =>
<a href="@url(repository)/pull/@pullRequest.issueId" class="btn btn-pullrequest-branch btn-mini" title="@issue.title" data-toggle="tooltip">#@pullRequest.issueId</a>
<a href="@url(repository)/pull/@pullRequest.issueId" class="btn btn-small btn-pullrequest-branch" title="@issue.title" data-toggle="tooltip">#@pullRequest.issueId</a>
}.getOrElse{
<a href="@url(repository)/compare?head=@urlEncode(encodeRefName(branch))" class="btn btn-success btn-mini"><i class="icon-white icon-retweet" data-toggle="tooltip" title="Compare, review, create a pull request"></i></a>
<a href="@url(repository)/compare?head=@urlEncode(encodeRefName(branch))" class="btn btn-small btn-success"><i class="octicon octicon-git-compare" data-toggle="tooltip" title="Compare, review, create a pull request"></i></a>
}
@helper.html.branchcontrol(
branch,
@@ -44,7 +44,7 @@
</div>
<table class="table table-file-list">
<tr>
<th colspan="4" style="font-weight: normal;">
<th colspan="4" style="font-weight: normal; border: none;">
<a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a>
@if(latestCommit.description.isDefined){
<a href="javascript:void(0)" onclick="$('#description-@latestCommit.id').toggle();" class="omit">...</a>
@@ -66,7 +66,7 @@
</div>
@if(latestCommit.isDifferentFromAuthor) {
<div class="committer">
<span class="icon-arrow-right"></span>
<span class="octicon octicon-arrow-right"></span>
<span>@user(latestCommit.committerName, latestCommit.committerEmailAddress, "username strong")</span>
<span class="muted"> committed @helper.html.datetimeago(latestCommit.commitTime)</span>
</div>
@@ -77,7 +77,7 @@
</tr>
@if(pathList.size > 0){
<tr>
<td width="16"></td>
<td width="16" class="file-icon"></td>
<td><a href="@url(repository)@if(pathList.size > 1){/tree/@encodeRefName(branch)/@pathList.init.mkString("/")}">..</a></td>
<td></td>
<td></td>
@@ -85,7 +85,7 @@
}
@files.map { file =>
<tr>
<td width="16">
<td width="16" class="file-icon">
@if(file.isDirectory){
@if(file.linkUrl.isDefined){
<i class="octicon octicon-file-symlink-directory"></i>
@@ -96,7 +96,7 @@
<i class="octicon octicon-file-text"></i>
}
</td>
<td>
<td class="content-name">
@if(file.isDirectory){
@if(file.linkUrl.isDefined){
<a href="@file.linkUrl">
@@ -118,8 +118,7 @@
}
</td>
<td class="mute">
<a href="@url(repository)/commit/@file.commitId" class="commit-message">@link(file.message, repository)</a>
[@user(file.author, file.mailAddress)]
<a href="@url(repository)/commit/@file.commitId" class="commit-message shorten-text" title="@file.message">@link(file.message, repository)</a>
</td>
<td style="text-align: right;">@helper.html.datetimeago(file.time, false)</td>
</tr>
@@ -128,7 +127,7 @@
@readme.map { case(filePath, content) =>
<div id="readme">
<div class="box-header">@filePath.reverse.head</div>
<div class="box-content markdown-body">@renderMarkup(filePath, content, branch, repository, false, false, true)</div>
<div class="box-content-bottom markdown-body" style="padding-left: 16px; padding-right: 16px;">@renderMarkup(filePath, content, branch, repository, false, false, true)</div>
</div>
}
}

View File

@@ -27,8 +27,8 @@
<table id="tree-finder-results" class="table table-file-list" data-url="@url(repository)/tree-list/@treeId">
<tbody class="tree-browser-result-template">
<tr class="tree-browser-result">
<td class="icon"><span class="icon icon-chevron-right"></span></td>
<td class="icon"><img src="@assets/common/images/file.png"/></td>
<td class="icon"><span class="octicon octicon-chevron-right"></span></td>
<td class="icon"><i class="octicon octicon-file-text"></i></td>
<td>
<a href="@url(repository)/blob/@encodeRefName(branch)"></a>
</td>

View File

@@ -24,7 +24,7 @@
Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a>
@helper.html.datetimeago(issue.registeredDate)
@if(issue.commentCount > 0){
&nbsp;&nbsp;<i class="icon-comment"></i><span class="strong">@issue.commentCount</span> @plural(issue.commentCount, "comment")
&nbsp;&nbsp;<i class="octicon octicon-comment"></i><span class="strong">@issue.commentCount</span> @plural(issue.commentCount, "comment")
}
</div>
</div>

View File

@@ -91,9 +91,9 @@
</div>
</div>
*@
<fieldset>
<div class="align-right" style="margin-top: 20px;">
<input type="submit" class="btn btn-success" value="Apply changes"/>
</fieldset>
</div>
</form>
}
}

View File

@@ -1,29 +1,23 @@
@(systemSettings: gitbucket.core.service.SystemSettingsService.SystemSettings)(implicit context: gitbucket.core.controller.Context)
@import context._
<table class="table table-bordered">
<tr>
<th class="metal">
@if(systemSettings.allowAccountRegistration){
<div class="pull-right">
<a href="@path/register" class="btn btn-mini">Create new account</a>
</div>
}
Sign in
</th>
</tr>
<tr>
<td>
<form action="@path/signin" method="POST" validate="true">
<label for="userName">Username:</label>
<span id="error-userName" class="error"></span>
<input type="text" name="userName" id="userName" style="width: 95%" autofocus/>
<label for="password">Password:</label>
<span id="error-password" class="error"></span>
<input type="password" name="password" id="password" style="width: 95%"/>
<div>
<input type="submit" class="btn btn-success" value="Sign in"/>
</div>
</form>
</td>
</tr>
</table>
<div class="box-header">
@if(systemSettings.allowAccountRegistration){
<div class="pull-right">
<a href="@path/register" class="btn btn-mini">Create new account</a>
</div>
}
<span class="strong">Sign in</span>
</div>
<div class="box-content-bottom">
<form action="@path/signin" method="POST" validate="true">
<label for="userName">Username:</label>
<span id="error-userName" class="error"></span>
<input type="text" name="userName" id="userName" style="width: 95%" autofocus/>
<label for="password">Password:</label>
<span id="error-password" class="error"></span>
<input type="password" name="password" id="password" style="width: 95%"/>
<div>
<input type="submit" class="btn btn-success" value="Sign in"/>
</div>
</form>
</div>

View File

@@ -21,7 +21,7 @@
</ul>
<form action="@url(repository)/wiki/@if(page.isEmpty){_new} else {_edit}" method="POST" validate="true">
<span id="error-pageName" class="error"></span>
<input type="text" name="pageName" value="@pageName" style="width: 850px; font-weight: bold;" placeholder="Input a page name."/>
<input type="text" name="pageName" value="@pageName" style="width: 910px; font-weight: bold;" placeholder="Input a page name."/>
@helper.html.preview(
repository = repository,
content = page.map(_.content).getOrElse(""),
@@ -29,11 +29,11 @@
enableRefsLink = false,
enableTaskList = false,
hasWritePermission = false,
style = "width: 850px; height: 400px;",
style = "width: 910px; height: 400px;",
styleClass = "monospace",
placeholder = ""
)
<input type="text" name="message" value="" style="width: 850px;" placeholder="Write a small message here explaining this change. (Optional)"/>
<input type="text" name="message" value="" style="width: 910px;" placeholder="Write a small message here explaining this change. (Optional)"/>
<input type="hidden" name="currentPageName" value="@pageName"/>
<input type="hidden" name="id" value="@page.map(_.id)"/>
<input type="submit" value="Save" class="btn btn-success">

View File

@@ -25,20 +25,23 @@
</li>
</ul>
<div style="width: 200px;" class="pull-right">
<table class="table table-bordered">
<tr>
<th class="metal">Pages <span class="label">@pages.length</span></th>
</tr>
<tr>
<td>
<ul style="margin-left: 0px; margin-bottom: 0px; word-break: break-all; width: 182px;">
@pages.map { page =>
<li style="margin-left:0px; list-style-type: none;"><a href="@url(repository)/wiki/@urlEncode(page)">@page</a></li>
}
</ul>
</td>
</tr>
</table>
@defining(15){ max =>
<div class="box-header">
<span class="strong">Pages</span> <span class="label">@pages.length</span>
</div>
<div class="box-content-bottom" style="padding: 0px;">
@pages.zipWithIndex.map { case (page, i) =>
<div class="box-content-row page-link" style="border: none; @if(i > max - 1){display:none;}">
<a href="@url(repository)/wiki/@urlEncode(page)" class="strong">@page</a>
</div>
}
@if(pages.size > max){
<div class="box-content-row show-more">
<a href="javascript:void(0);" id="show-more-pages">Show more @{pages.size - max} pages...</a>
</div>
}
</div>
}
<div class="small">
<strong>Clone this wiki locally</strong>
</div>
@@ -51,16 +54,21 @@
</div>
}
</div>
<div style="width: 650px;" class="pull-left">
<div style="width: 700px;" class="pull-left">
<div class="markdown-body">
@markdown(page.content, repository, true, false, false, false, pages)
</div>
</div>
}
}
@if(settings.ssh && loginAccount.isDefined){
<script>
$(function(){
<script>
$(function(){
$('#show-more-pages').click(function(e){
$('div.page-link').show();
$(e.target).parents('div.show-more').remove();
});
@if(settings.ssh && loginAccount.isDefined){
$('#repository-url-http').click(function(){
$('#repository-url').val('@httpUrl(repository)');
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
@@ -69,6 +77,6 @@
$('#repository-url').val('@sshUrl(repository, settings, loginAccount.get.userName)');
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
});
});
</script>
}
}
});
</script>

View File

@@ -60,6 +60,10 @@ h6 {
.head .octicon,.head .mega-octicon{
color : #BBB;
}
.align-right {
text-align: right;
}
/* ======================================================================== */
/* Global Header */
/* ======================================================================== */
@@ -131,15 +135,6 @@ a.global-header-menu {
line-height: 3.5;
}
/*
img.plugin-global-menu {
width: 16px;
height: 16px;
position: relative;
top: -2px;
}
*/
/* ======================================================================== */
/* General Styles */
/* ======================================================================== */
@@ -155,7 +150,7 @@ div.head div.forked {
}
div.container {
width: 920px;
width: 980px;
}
div.container-wide {
@@ -184,6 +179,12 @@ span.error {
font-weight: bold;
}
.shorten-text {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
a.commit-message, a.commit-id, a.username, a.issue-comment-count {
color: #333333;
}
@@ -199,6 +200,15 @@ a.omit:hover {
background-color: #aaa;
}
div.show-more {
text-align: center;
border-top: 1px solid silver;
}
div.show-more a {
color: #7aa1d3;
}
span.count-right {
float: right;
font-weight: bold;
@@ -227,82 +237,87 @@ h3 {
margin-top: 0px;
}
div.box {
background-color: #efefef;
padding: 2px;
margin-bottom: 10px;
}
div.box-header {
font-size: 120%;
font-weight: bold;
background-color: #e0e0e0;
background-image: -moz-linear-gradient(#fafafa, #e0e0e0);
background-image: -webkit-linear-gradient(#fafafa, #e0e0e0);
background-image: linear-gradient(#fafafa, #e0e0e0);
background-repeat: repeat-x;
background-color: #f5f5f5;
margin: 0;
border-top-left-radius: 1px;
border-top-right-radius: 1px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
border: 1px solid #d8d8d8;
border-bottom: none;
padding: 8px 10px 8px 10px;
text-shadow: 0 1px 0 #fff
}
div.box-header-small {
background-color: #e0e0e0;
background-image: -moz-linear-gradient(#fafafa, #e0e0e0);
background-image: -webkit-linear-gradient(#fafafa, #e0e0e0);
background-image: linear-gradient(#fafafa, #e0e0e0);
background-repeat: repeat-x;
margin: 0;
border-top-left-radius: 1px;
border-top-right-radius: 1px;
border: 1px solid #d8d8d8;
border-bottom: none;
padding: 6px 4px 6px 4px;
text-shadow: 0 1px 0 #fff
padding: 8px 8px 8px 8px;
}
.inline-comment div.box-header-small {
background: #f2f8fa;
}
.commit-list {
position: relative;
}
.commit-list::before {
position: absolute;
top: 0;
bottom: 0;
left: 6px;
z-index: -1;
display: block;
width: 2px;
content: "";
background-color: #f3f3f3;
}
ul.nav-pills-group .link {
color: #4078c0;
}
div.box-commits {
background-color: white;
border: 1px solid #d8d8d8;
padding-left: 0px;
padding-right: 0px;
margin-top: 10px;
margin-bottom: 10px;
margin-left: 20px;
}
div.box-content {
background-color: white;
border: 1px solid #d8d8d8;
padding: 4px;
border-radius: 3px;
}
th.box-header {
background-color: #e0e0e0;
background-image: -moz-linear-gradient(#fafafa, #e0e0e0);
background-image: -webkit-linear-gradient(#fafafa, #e0e0e0);
background-image: linear-gradient(#fafafa, #e0e0e0);
background-repeat: repeat-x;
margin: 0;
border-top-left-radius: 1px;
border-top-right-radius: 1px;
div > div.box-content-row:nth-of-type(1) {
border: none;
}
div.box-content-row {
border-top: 1px solid #d8d8d8;
padding: 4px;
}
div.repo-link {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
div.box-content-bottom {
background-color: white;
border: 1px solid #d8d8d8;
border-bottom: none;
text-shadow: 0 1px 0 #fff
padding: 4px;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
margin-bottom: 20px;
}
table.table th.box-header {
background-color: #f5f5f5;
}
th.box-header .octicon {
display: inline;
}
th.metal {
background-color: #e0e0e0;
background-image: -moz-linear-gradient(#fafafa, #e8e8e8);
background-image: -webkit-linear-gradient(#fafafa, #e8e8e8);
background-image: linear-gradient(#fafafa, #e8e8e8);
background-repeat: repeat-x;
margin: 0;
border-top-left-radius: 1px;
border-top-right-radius: 1px;
display: inline;
}
dl {
@@ -374,6 +389,22 @@ span.highlight {
cursor: pointer;
}
.btn .octicon {
color: #333;
}
.btn-success .octicon {
color: #fff;
}
.btn-warning .octicon {
color: #fff;
}
.btn-danger .octicon {
color: #fff;
}
/****************************************************************************/
/* Side Menu */
/****************************************************************************/
@@ -381,23 +412,30 @@ ul.sidemenu {
margin-left: 0px;
}
ul.sidemenu a:hover{
ul.sidemenu a {
display: block;
padding: 8px 10px;
}
ul.sidemenu a:hover {
text-decoration: none;
}
ul.sidemenu li.active {
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
border-right: 2px solid #bb4444;
border-right: 3px solid #bb4444;
border-left: 1px solid white;
}
ul.sidemenu div.gradient {
width: 5px;
height: 30px;
background: linear-gradient(to right, #eee, #fff);
margin-right: 4px;
border-left: 1px solid #eee;
ul.sidemenu li.active a {
background-color: #fff;
}
ul.sidemenu {
background-image: -webkit-linear-gradient(left, #f6f6f6 0%, #fff 8px);
background-image: linear-gradient(to right, #f6f6f6 0%, #fff 8px);
box-shadow: inset 1px 0 0 #eee;
}
ul.sidemenu div.margin {
@@ -408,8 +446,6 @@ ul.sidemenu div.margin {
}
ul.sidemenu li {
line-height: 30px;
height: 30px;
border-left: 1px solid #eee;
margin-left:0px;
border-right: 2px solid white;
@@ -485,36 +521,24 @@ ul.nav-stacked.side-menu li.active a:hover {
}
.nav-tabs.nav-stacked.side-menu > li:first-child > a {
-webkit-border-top-right-radius: 0px;
border-top-right-radius: 0px;
-webkit-border-top-left-radius: 0px;
border-top-left-radius: 0px;
-moz-border-radius-topright: 0px;
-moz-border-radius-topleft: 0px;
border-top-right-radius: 3px;
border-top-left-radius: 3px;
}
.nav-tabs.nav-stacked.side-menu > li:last-child > a {
-webkit-border-bottom-right-radius: 0px;
border-bottom-right-radius: 0px;
-webkit-border-bottom-left-radius: 0px;
border-bottom-left-radius: 0px;
-moz-border-radius-bottomright: 0px;
-moz-border-radius-bottomleft: 0px;
border-bottom-right-radius: 3px;
border-bottom-left-radius: 3px;
}
/****************************************************************************/
/* Repositories */
/****************************************************************************/
div.repository-icon {
position: absolute;
position: absolute;
}
div.repository-content {
margin-left: 40px;
margin-left: 40px;
}
.branches .muted-link{
@@ -604,15 +628,11 @@ p.description {
a.header-link {
color: #888;
font-size: 90%;
}
a.header-link i.octicon {
opacity: 0.5;
display: block;
}
a.header-link strong {
color: black;
font-size: 90%;
}
a.header-link:hover {
@@ -639,14 +659,17 @@ a.header-link:hover i.octicon-x{
color: #FFF;
}
/*
table.blobview {
table-layout: fixed;
}
*/
table.table-file-list {
margin-bottom: 0px;
border: 1px solid #ddd;
border-radius: 3px;
border-collapse: separate;
}
table.table-file-list th, table.table-file-list td {
@@ -665,6 +688,25 @@ table.table-file-list td {
font-size: small;
}
table.table-file-list .file-icon {
padding-right: 1px;
}
table.table-file-list .content-name {
max-width: 180px;
}
table.table-file-list .commit-message {
max-width: 415px;
display: inline-block;
}
table.table-file-list .commit-author {
color: #999;
font-size: 12px;
float: right;
}
th, td, .table th, .table td {
padding-top: 4px;
padding-bottom: 4px;
@@ -876,6 +918,10 @@ a.issue-title {
font-size: 120%;
}
.label-success {
background-color: #6cc644;
}
ul.label-list {
list-style-type: none;
padding-left: 0px;
@@ -979,8 +1025,16 @@ div.issue-info {
}
div.issue-content {
padding: 8px;
background-color: #fbfbfb;
padding: 13px;
background-color: #fff;
}
div.issue-content p:first-child {
margin-top: 0;
}
div.issue-content p:last-child {
margin-bottom: 0;
}
h4#issueTitle {
@@ -1007,17 +1061,47 @@ div.issue-participants {
div.issue-comment-box, div.commit-comment-box {
margin-bottom: 15px;
margin-left: 50px;
margin-left: 70px;
max-width: 820px;
}
div.issue-comment-box textarea {
width: 670px;
height: 100px;
max-height: 300px;
}
div.issue-comment-action {
padding-bottom: 10px;
padding-top: 0px;
margin-left: 70px;
margin-bottom: 10px;
border-bottom: 4px solid #ddd;
}
div.issue-comment-action .octicon {
border-radius: 50%;
background-color: #f3f3f3;
color: #767676;
font-size: 18px;
margin-right: 7px;
width: 28px;
height: 28px;
line-height: 28px;
text-align: center;
border: 2px solid #fff;
}
div.issue-reopened .octicon {
background-color: #6cc644;
color: #fff;
}
div.issue-comment-action .octicon.danger {
background-color: #bd2c00;
color: #fff;
}
.nav-pills > li > span.issue-label {
display: block;
padding: 0px 8px 2px 8px;
@@ -1406,6 +1490,7 @@ div.markdown-body {
font: 15px Helvetica, arial, freesans, clean, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol";
word-wrap: break-word;
}
div.markdown-body h1 {
border-bottom: 1px solid #ddd;
font-size: 2.5em;
@@ -1449,13 +1534,17 @@ div.markdown-body pre {
white-space: pre;
word-wrap: normal;
overflow: auto;
border: none;
padding: 4px;
}
div.markdown-body code {
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 12px;
padding: 0 5px;
padding: 4px;
background-color: rgba(0,0,0,0.04);
rgb(51, 51, 51);
border: none;
color: black;
}
div.markdown-body table {
@@ -1592,13 +1681,15 @@ div.markdown-body table colgroup + tbody tr:first-child td:last-child {
}
.markdown-head {
left: -18px;
padding-left: 18px;
position: relative;
line-height: 1.7;
}
a.markdown-anchor-link {
position: absolute;
left: -18px;
left: 0px;
display: none;
color: #999;
/* From octicon style */
@@ -1676,9 +1767,12 @@ h6 a.markdown-anchor-link {
padding-left: 50px;
}
/*
div.container.blame-container{
width:1270px;
}
*/
.line-age-legend {
display: none;
}

View File

@@ -23,18 +23,9 @@ $(function(){
$(e.target).children('a.markdown-anchor-link').show();
});
$('.markdown-head').mouseleave(function(e){
var anchorLink = $(e.target).children('a.markdown-anchor-link');
if(anchorLink.data('active') !== true){
anchorLink.hide();
}
$(e.target).children('a.markdown-anchor-link').hide();
});
$('a.markdown-anchor-link').mouseenter(function(e){
$(e.target).data('active', true);
});
$('a.markdown-anchor-link').mouseleave(function(e){
$(e.target).data('active', false);
$(e.target).hide();
});
@@ -360,12 +351,12 @@ function scrollIntoView(target){
}
}
/**
* escape html
*/
function escapeHtml(text){
return text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;').replace(/>/g,'&gt;');
}
///**
// * escape html
// */
//function escapeHtml(text){
// return text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;').replace(/>/g,'&gt;');
//}
/**
* calculate string ranking for path.

View File

@@ -231,13 +231,13 @@ body {
}
a {
color: #0088cc;
color: #4078c0;
text-decoration: none;
}
a:hover,
a:focus {
color: #005580;
color: #4078c0;
text-decoration: underline;
}

0
src/main/webapp/assets/vendors/octicons/README.md vendored Normal file → Executable file
View File

BIN
src/main/webapp/assets/vendors/octicons/octicons-local.ttf vendored Normal file → Executable file

Binary file not shown.

29
src/main/webapp/assets/vendors/octicons/octicons.css vendored Normal file → Executable file
View File

@@ -29,9 +29,6 @@
.mega-octicon { font-size: 32px; }
.octicon-alert:before { content: '\f02d'} /*  */
.octicon-alignment-align:before { content: '\f08a'} /*  */
.octicon-alignment-aligned-to:before { content: '\f08e'} /*  */
.octicon-alignment-unalign:before { content: '\f08b'} /*  */
.octicon-arrow-down:before { content: '\f03f'} /*  */
.octicon-arrow-left:before { content: '\f040'} /*  */
.octicon-arrow-right:before { content: '\f03e'} /*  */
@@ -40,7 +37,9 @@
.octicon-arrow-small-right:before { content: '\f071'} /*  */
.octicon-arrow-small-up:before { content: '\f09f'} /*  */
.octicon-arrow-up:before { content: '\f03d'} /*  */
.octicon-beer:before { content: '\f069'} /*  */
.octicon-microscope:before,
.octicon-beaker:before { content: '\f0dd'} /*  */
.octicon-bell:before { content: '\f0de'} /*  */
.octicon-book:before { content: '\f007'} /*  */
.octicon-bookmark:before { content: '\f07b'} /*  */
.octicon-briefcase:before { content: '\f0d3'} /*  */
@@ -69,6 +68,8 @@
.octicon-dash:before { content: '\f0ca'} /*  */
.octicon-dashboard:before { content: '\f07d'} /*  */
.octicon-database:before { content: '\f096'} /*  */
.octicon-clone:before,
.octicon-desktop-download:before { content: '\f0dc'} /*  */
.octicon-device-camera:before { content: '\f056'} /*  */
.octicon-device-camera-video:before { content: '\f057'} /*  */
.octicon-device-desktop:before { content: '\f27c'} /*  */
@@ -113,7 +114,6 @@
.octicon-history:before { content: '\f07e'} /*  */
.octicon-home:before { content: '\f08d'} /*  */
.octicon-horizontal-rule:before { content: '\f070'} /*  */
.octicon-hourglass:before { content: '\f09e'} /*  */
.octicon-hubot:before { content: '\f09d'} /*  */
.octicon-inbox:before { content: '\f0cf'} /*  */
.octicon-info:before { content: '\f059'} /*  */
@@ -121,10 +121,6 @@
.octicon-issue-opened:before { content: '\f026'} /*  */
.octicon-issue-reopened:before { content: '\f027'} /*  */
.octicon-jersey:before { content: '\f019'} /*  */
.octicon-jump-down:before { content: '\f072'} /*  */
.octicon-jump-left:before { content: '\f0a5'} /*  */
.octicon-jump-right:before { content: '\f0a6'} /*  */
.octicon-jump-up:before { content: '\f073'} /*  */
.octicon-key:before { content: '\f049'} /*  */
.octicon-keyboard:before { content: '\f00d'} /*  */
.octicon-law:before { content: '\f0d8'} /*  */
@@ -146,15 +142,10 @@
.octicon-markdown:before { content: '\f0c9'} /*  */
.octicon-megaphone:before { content: '\f077'} /*  */
.octicon-mention:before { content: '\f0be'} /*  */
.octicon-microscope:before { content: '\f089'} /*  */
.octicon-milestone:before { content: '\f075'} /*  */
.octicon-mirror-public:before,
.octicon-mirror:before { content: '\f024'} /*  */
.octicon-mortar-board:before { content: '\f0d7'} /*  */
.octicon-move-down:before { content: '\f0a8'} /*  */
.octicon-move-left:before { content: '\f074'} /*  */
.octicon-move-right:before { content: '\f0a9'} /*  */
.octicon-move-up:before { content: '\f0a7'} /*  */
.octicon-mute:before { content: '\f080'} /*  */
.octicon-no-newline:before { content: '\f09c'} /*  */
.octicon-octoface:before { content: '\f008'} /*  */
@@ -166,21 +157,15 @@
.octicon-person-follow:before,
.octicon-person:before { content: '\f018'} /*  */
.octicon-pin:before { content: '\f041'} /*  */
.octicon-playback-fast-forward:before { content: '\f0bd'} /*  */
.octicon-playback-pause:before { content: '\f0bb'} /*  */
.octicon-playback-play:before { content: '\f0bf'} /*  */
.octicon-playback-rewind:before { content: '\f0bc'} /*  */
.octicon-plug:before { content: '\f0d4'} /*  */
.octicon-repo-create:before,
.octicon-gist-new:before,
.octicon-file-directory-create:before,
.octicon-file-add:before,
.octicon-plus:before { content: '\f05d'} /*  */
.octicon-podium:before { content: '\f0af'} /*  */
.octicon-primitive-dot:before { content: '\f052'} /*  */
.octicon-primitive-square:before { content: '\f053'} /*  */
.octicon-pulse:before { content: '\f085'} /*  */
.octicon-puzzle:before { content: '\f0c0'} /*  */
.octicon-question:before { content: '\f02c'} /*  */
.octicon-quote:before { content: '\f063'} /*  */
.octicon-radio-tower:before { content: '\f030'} /*  */
@@ -201,16 +186,15 @@
.octicon-search:before { content: '\f02e'} /*  */
.octicon-server:before { content: '\f097'} /*  */
.octicon-settings:before { content: '\f07c'} /*  */
.octicon-shield:before { content: '\f0e1'} /*  */
.octicon-log-in:before,
.octicon-sign-in:before { content: '\f036'} /*  */
.octicon-log-out:before,
.octicon-sign-out:before { content: '\f032'} /*  */
.octicon-split:before { content: '\f0c6'} /*  */
.octicon-squirrel:before { content: '\f0b2'} /*  */
.octicon-star-add:before,
.octicon-star-delete:before,
.octicon-star:before { content: '\f02a'} /*  */
.octicon-steps:before { content: '\f0c7'} /*  */
.octicon-stop:before { content: '\f08f'} /*  */
.octicon-repo-sync:before,
.octicon-sync:before { content: '\f087'} /*  */
@@ -231,6 +215,7 @@
.octicon-unfold:before { content: '\f039'} /*  */
.octicon-unmute:before { content: '\f0ba'} /*  */
.octicon-versions:before { content: '\f064'} /*  */
.octicon-watch:before { content: '\f0e0'} /*  */
.octicon-remove-close:before,
.octicon-x:before { content: '\f081'} /*  */
.octicon-zap:before { content: '\26A1'} /* ⚡ */

BIN
src/main/webapp/assets/vendors/octicons/octicons.eot vendored Normal file → Executable file

Binary file not shown.

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