Compare commits

...

190 Commits

Author SHA1 Message Date
Naoki Takezoe
a016e99843 Merge pull request #1036 from Nexis81/maintenance/3.10
Update web.xml
2016-01-07 20:40:44 +09:00
Nexis81
945bfc75de Update web.xml
The application name was missing from the application listing in the Web Application Manager for Tomcat.
2016-01-07 03:15:41 -08:00
Naoki Takezoe
89f3e66785 Update version number to 3.10.1 2016-01-01 14:28:42 +09:00
Naoki Takezoe
61670ddc9e Fix dropdown bug 2016-01-01 14:23:28 +09:00
Naoki Takezoe
1ddcc36778 Remove file appender configuration 2016-01-01 13:21:58 +09:00
Naoki Takezoe
990d082422 3.10 release 2015-12-30 01:18:13 +09:00
Naoki Takezoe
dfe3fbc02f Bump up H2 to 1.4.190 2015-12-29 01:37:52 +09:00
Naoki Takezoe
c077dd0046 Rename logback-test.xml to logback-dev.xml 2015-12-27 14:40:54 +09:00
Naoki Takezoe
9cc6beead7 (refs #990)Provides path for raw contents instead of ?raw=true 2015-12-26 22:48:28 +09:00
Naoki Takezoe
92f6792ad9 Adjust format 2015-12-26 21:20:09 +09:00
Naoki Takezoe
25031eadfb Merge pull request #1017 from swaldman/minor-c3p0-tweaks
Upgrade c3p0 and tweak default config to avoid Tomcat issues
2015-12-26 17:00:33 +09:00
Naoki Takezoe
a7024615e1 Merge pull request #1020 from P1tt187/sshd-RSA-keyprovider
update sshd dependency, rsa keyprovider as default
2015-12-26 16:59:30 +09:00
Naoki Takezoe
8191a4bedb (refs #1004)Fix edit webhook link url 2015-12-26 03:25:49 +09:00
Naoki Takezoe
7e1f5aa353 Fix branch control dropdown 2015-12-25 10:26:30 +09:00
Fabian Markert
44e161f6ec update version 2015-12-24 17:16:20 +01:00
Fabian Markert
7bd17ed038 update sshd dependency
migrate to new sshd api
change the default key provider algorithm to RSA
2015-12-24 17:11:40 +01:00
Naoki Takezoe
a47404130b Don't break lines in Wiki or markdown files on the repository viewer 2015-12-24 02:58:39 +09:00
Naoki Takezoe
669949aa8c Update docs 2015-12-23 23:30:10 +09:00
Naoki Takezoe
c773bb90f6 Update docs 2015-12-23 23:26:59 +09:00
Naoki Takezoe
18f525523b Update docs 2015-12-23 23:06:22 +09:00
Naoki Takezoe
3bfd228df1 Update docs 2015-12-23 23:03:32 +09:00
Naoki Takezoe
c072ab2dc5 Update docs 2015-12-23 22:44:20 +09:00
Naoki Takezoe
53e06c5e86 (refs #1018)Move to xsbt-web-plugin from scalatra-sbt 2015-12-23 21:37:25 +09:00
Steve Waldman
b0a749bdc1 Upgrade c3p0 and tweak default config to avoid Tomcat issues
Adds a standard typesafe-config reference.conf file that sets c3p0
config to prevents Tomcat memory leaks on hot-redeploy/shutdown.

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

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

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

Updates c3p0 version to c3p0-0.9.5.2, which fixes that leads to
unnecessary String allocation when logging via slf4j.
2015-12-21 22:51:12 -08:00
Naoki Takezoe
809443a46f Zip logback.xml into the root of gitbucket.war 2015-12-22 07:59:47 +09:00
Naoki Takezoe
59ef44dd71 Update logging configuration 2015-12-22 07:59:15 +09:00
Naoki Takezoe
d77973a29e Remove IDE plugins and add logback-classic 2015-12-22 07:58:30 +09:00
Naoki Takezoe
be6bc16d3d Fix global header style 2015-12-21 15:14:51 +09:00
Naoki Takezoe
407e926abe Fix styles in file editing form in the repository viewer 2015-12-21 15:14:21 +09:00
Naoki Takezoe
117655b011 (refs #1002)Upgrade markedj to 1.0.6-SNAPSHOT to solve IndexOutOfBoundsException in the table 2015-12-19 12:56:34 +09:00
Naoki Takezoe
2ea4a5a7ef (refs #1007)Fix max length constraint for repository owner and name at the repository creation form 2015-12-18 01:46:25 +09:00
Naoki Takezoe
30b0279efe (refs #1005)Fix typo 2015-12-18 01:40:32 +09:00
Naoki Takezoe
7511701bb2 Merge pull request #1009 from team-lab/fix/webhook-push-pusher
(fixes #1008) pusher of webhook push event is not same as github's it
2015-12-18 01:32:18 +09:00
Naoki Takezoe
a919ae3430 Merge pull request #1010 from team-lab/suppot-legacy-statuses-api
support legacy route of List Statuses for a specific Ref api
2015-12-18 01:31:28 +09:00
nazoking
c30ee24f8d support legacy route of List Statuses for a specific Ref api 2015-12-16 21:54:47 +09:00
nazoking
10b5de571e (fixes #1008) pusher of webhook push event is not same as github's it 2015-12-16 21:41:14 +09:00
Naoki Takezoe
c6f4ec7250 Fix header-link style 2015-12-15 19:51:16 +09:00
Naoki Takezoe
b229058726 Fix fork button group style 2015-12-15 19:39:18 +09:00
Naoki Takezoe
21363794b6 Merge pull request #999 from gitbucket/css_bootstrap3
Move to Bootstrap3 with GitHub theme
2015-12-15 15:58:54 +09:00
Naoki Takezoe
1754d37ac0 Fix style of circle icons 2015-12-15 15:13:54 +09:00
Naoki Takezoe
1c36b185ec Fix width and font of the dropzone 2015-12-15 14:56:24 +09:00
Naoki Takezoe
1a4b94e5df Fix modal dialog 2015-12-15 11:46:47 +09:00
Naoki Takezoe
31bd8e1741 Fix button size 2015-12-14 18:48:22 +09:00
Naoki Takezoe
e8824ad1ac Imporve styles for mobile 2015-12-14 01:56:15 +09:00
Naoki Takezoe
17636a5ecf Fixup 2015-12-14 01:11:42 +09:00
Naoki Takezoe
8f36ff60de Use bootstrap panel instead of custom box styles 2015-12-14 00:16:18 +09:00
Naoki Takezoe
7ad605afba Fix style of issue comment form and more 2015-12-13 23:59:42 +09:00
Naoki Takezoe
f080e8693e Fix right margin of account search box 2015-12-12 13:20:05 +09:00
Naoki Takezoe
e27bf7868f Tweak pagination padding 2015-12-10 17:17:18 +09:00
Naoki Takezoe
db55fe9ff3 Fixup 2015-12-10 16:02:58 +09:00
Naoki Takezoe
de4db0764c Use Bootstrap panel instead of custom CSS 2015-12-10 14:53:19 +09:00
Naoki Takezoe
9d644b4625 Fix buttons of webhook list 2015-12-10 14:04:44 +09:00
Naoki Takezoe
354dc27e84 Use Bootstrap panel instead of custom CSS 2015-12-10 12:13:22 +09:00
Naoki Takezoe
1c639474d3 Fix milestone list and progress bar 2015-12-10 12:03:22 +09:00
Naoki Takezoe
4d8b47732e Remove unused bootstrap files 2015-12-10 02:03:46 +09:00
Naoki Takezoe
8650abf679 Fix milestone creation / editing form 2015-12-10 02:00:42 +09:00
Naoki Takezoe
27dcc2ef48 Fix table vertical border 2015-12-10 01:16:17 +09:00
Naoki Takezoe
94a12cd28c Fix label editing form 2015-12-09 19:16:34 +09:00
Naoki Takezoe
a982b64fd4 Use Bootstrap panels instead of GitBucket original styles 2015-12-09 18:29:41 +09:00
Naoki Takezoe
ee5b4bbf2e Fix styles 2015-12-09 02:48:10 +09:00
Naoki Takezoe
d09ddd8d07 Fix tables and some pages 2015-12-08 19:21:08 +09:00
Naoki Takezoe
c7bf47820c Use customized Bootstrap3 theme 2015-12-08 09:33:22 +09:00
Naoki Takezoe
4dfb01d59e Fix typo 2015-12-08 02:35:52 +09:00
Naoki Takezoe
54bef6505f Fix issue search box 2015-12-08 02:26:11 +09:00
Naoki Takezoe
2aa71ed217 Fix borderd table style 2015-12-08 01:59:31 +09:00
Naoki Takezoe
d90938421f Fix ZeroClipboard button 2015-12-07 23:19:30 +09:00
Naoki Takezoe
e4992bbd17 Fix styles of repository setting pages 2015-12-07 16:56:38 +09:00
Naoki Takezoe
7853b22522 Fix sidebar style 2015-12-07 16:01:03 +09:00
Naoki Takezoe
9475215fa6 Fix styles of plugins page 2015-12-07 15:20:06 +09:00
Naoki Takezoe
8622bbd2ac Fix styles of account and group creation page 2015-12-07 14:38:18 +09:00
Naoki Takezoe
0cdcc252e0 Merge branch 'master' into css_bootstrap3
Conflicts:
	src/main/twirl/gitbucket/core/account/edit.scala.html
2015-12-07 08:33:22 +09:00
Naoki Takezoe
9477cca8e8 Fix box style 2015-12-05 19:54:32 +09:00
Naoki Takezoe
176e427058 Update README.md for 3.9 release 2015-12-05 19:48:44 +09:00
Naoki Takezoe
1bf26cacb9 Update version to 3.9.0 2015-12-05 19:46:29 +09:00
Naoki Takezoe
38e8247483 Fix styles of system administration page 2015-12-05 19:45:07 +09:00
Naoki Takezoe
44aa12108c Fix styles of the repository creation page and more 2015-12-05 14:54:50 +09:00
Naoki Takezoe
dd73405aa9 Fix styles 2015-12-05 12:41:55 +09:00
Naoki Takezoe
6710393a53 Merge branch 'master' into css_bootstrap3 2015-12-05 11:53:15 +09:00
Naoki Takezoe
711aee1c8b Fix markedj version as 1.0.5 2015-12-05 03:54:51 +09:00
Naoki Takezoe
51be1048d5 (refs #987)Remove user related data when users delete themselves 2015-12-05 03:45:26 +09:00
Naoki Takezoe
50166f04d8 Disable auto completion in the select user field 2015-12-05 03:43:57 +09:00
Naoki Takezoe
7eb6fea08b Fix typo in comment 2015-12-05 03:12:21 +09:00
Naoki Takezoe
8e3c054da4 Merge pull request #983 from team-lab/feature/webhook-review-comment
Send webhook on create pull request review comment
2015-12-05 01:14:54 +09:00
nazoking
5249b67df1 Merge branch 'master' into feature/webhook-review-comment 2015-12-03 20:09:46 +09:00
Naoki Takezoe
583830c1c2 Fix styles 2015-12-01 02:47:09 +09:00
Naoki Takezoe
bb75f1c034 Merge branch 'css_bootstrap3' of https://github.com/takezoe/gitbucket into css_bootstrap3 2015-12-01 02:12:08 +09:00
Naoki Takezoe
b7a9236283 Merge branch 'master' of https://github.com/takezoe/gitbucket into css_bootstrap3 2015-12-01 01:37:14 +09:00
Naoki Takezoe
7e0e172d37 Fix fork button 2015-11-30 23:22:16 +09:00
Naoki Takezoe
ea37a34476 Fix styles of issues and pull requests 2015-11-30 01:16:27 +09:00
Naoki Takezoe
fb5564de07 Fix diff styles 2015-11-27 12:53:10 +09:00
Naoki Takezoe
cead7e9e4e Fix commit list style 2015-11-27 12:37:26 +09:00
Naoki Takezoe
7cf23b6c56 Fix pull request creation form style 2015-11-27 12:37:16 +09:00
Naoki Takezoe
1080821862 Fix issue styles 2015-11-27 10:16:17 +09:00
Naoki Takezoe
0bf843ce2a Fix styles 2015-11-27 10:10:15 +09:00
Naoki Takezoe
b8bd8f5512 Fix branch list styles 2015-11-27 09:43:17 +09:00
Naoki Takezoe
12fba30ef8 Fix styles 2015-11-25 10:44:55 +09:00
Naoki Takezoe
dc5cb74f4d Merge branch 'master' of https://github.com/takezoe/gitbucket 2015-11-18 01:48:47 +09:00
Naoki Takezoe
446a524b5a Upgrade scalatra-forms to 0.2.0 to solve performance issue 2015-11-18 01:48:40 +09:00
Naoki Takezoe
121110aede Fix markdown header style 2015-11-17 23:19:08 +09:00
Naoki Takezoe
c76a8562ea Merge pull request #995 from uli-heller/patch-2
extensibility is probably the right word?
2015-11-17 19:28:00 +09:00
uli-heller
495d8069f5 extensibility is probably the right word? 2015-11-17 09:51:50 +01:00
Naoki Takezoe
70af6d0c35 Update README.md 2015-11-17 16:04:58 +09:00
Naoki Takezoe
abfc6eea1e Fix repository setting pages presentation 2015-11-17 01:52:14 +09:00
Naoki Takezoe
fd2f3e252c Fix webhook editing presentation and performance issue 2015-11-17 01:51:55 +09:00
Naoki Takezoe
3b6d0065a2 Once delete editHooks.scala.html to rename 2015-11-17 01:48:13 +09:00
Naoki Takezoe
7ffe2ab864 Fix global header style 2015-11-15 12:54:34 +09:00
Naoki Takezoe
2f6febb748 Adjust styles for Bootstrap3 2015-11-15 02:27:55 +09:00
Naoki Takezoe
092c897150 Fix sidebar style 2015-11-15 02:05:24 +09:00
Naoki Takezoe
30f072f925 Merge pull request #992 from team-lab/fix/lost-webhook-events-on-rename-repository
fix lost web hook events on repository renamed.
2015-11-14 15:02:41 +09:00
nazoking
72d0282613 Merge branch 'master' into feature/webhook-review-comment 2015-11-14 13:27:40 +09:00
nazoking
a56f766497 fix lost web hook events on repository renamed. 2015-11-14 13:16:13 +09:00
Naoki Takezoe
31b5583896 Fix header styles 2015-11-13 02:32:04 +09:00
Naoki Takezoe
7c95bfc77e Merge branch 'master' into css_bootstrap3 2015-11-12 03:45:00 +09:00
Naoki Takezoe
c6804fe589 Add bootstrap-theme-github 2015-11-12 03:44:37 +09:00
Naoki Takezoe
fc41e282ce Merge pull request #973 from gitbucket/commit_comment_count
Count COMMIT_COMMENT as issue comment
2015-11-10 11:48:20 +09:00
Naoki Takezoe
d3b21a4e39 Drop PULL_REQUEST column from COMMIT_COMMENT table 2015-11-10 11:35:59 +09:00
Naoki Takezoe
7b6d9850a2 Moving to Bootstrap3 2015-11-10 11:06:11 +09:00
nazoking
84d97817e3 add comment 2015-11-09 13:57:37 +09:00
nazoking
2c6d2176bb fix space 2015-11-09 13:57:26 +09:00
nazoking
7840d28ed2 add webhook pull-request-review-comment 2015-11-09 12:20:07 +09:00
Naoki Takezoe
d4fd2e6cd6 Remove bottom margin of inline comment in diff view 2015-11-09 01:20:44 +09:00
Naoki Takezoe
70e8385246 (refs #974)Not hide add-comment icon when adding comment form is active. 2015-11-09 01:15:04 +09:00
Naoki Takezoe
3fd23f1749 Fix markdown preview style 2015-11-08 22:26:43 +09:00
Naoki Takezoe
a3e3aea4e0 Fix styles 2015-11-08 19:08:05 +09:00
Naoki Takezoe
dcfb8ad8eb (refs #976)Enable GFM line breaks 2015-11-08 18:03:44 +09:00
Naoki Takezoe
35f82e91bc (refs #976)Upgrade markedj to 1.0.5-SNAPSHOT 2015-11-08 17:49:25 +09:00
Naoki Takezoe
f1533cb168 Count COMMIT_COMMENT as issue comment 2015-11-08 04:06:14 +09:00
Naoki Takezoe
763fbec0ca Update README.md 2015-11-08 02:58:54 +09:00
Naoki Takezoe
111c893d94 Merge pull request #968 from justinpitts/master
Correct typo on system administration page.
2015-11-07 03:35:39 +09:00
Naoki Takezoe
80c38a45c5 Merge pull request #941 from team-lab/feature/webhook-scope
feature/can select event trigger of webhook.
2015-11-07 03:35:09 +09:00
nazoking
548860607b update migration version to 3.9 from 3.8 2015-11-06 14:52:36 +09:00
nazoking
66b5fe7337 Merge branch 'master' into feature/webhook-scope 2015-11-06 14:48:03 +09:00
Justin Pitts
2b2a117912 Correct typo on system administration page. 2015-11-04 08:51:48 -05:00
Naoki Takezoe
39a10fd167 Update README.md 2015-11-04 02:00:14 +09:00
Naoki Takezoe
93f8dbd42a Fix markedj version 2015-11-02 08:58:29 +09:00
Naoki Takezoe
4f280d0df8 Fix blockquote font size 2015-11-02 02:33:06 +09:00
Naoki Takezoe
5e9ff3278a Fix the default number of displayed repositories 2015-11-02 00:32:46 +09:00
Naoki Takezoe
b4b68f0e17 Update README.md 2015-10-31 11:55:16 +09:00
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
nazoking
a08a212fdc feature/can select webhook events 2015-10-13 18:48:02 +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
kanmi
1fddc01f6e Specify option to generate an RSA key 2015-09-16 01:28:31 +09:00
163 changed files with 24482 additions and 12582 deletions

View File

@@ -1,5 +1,7 @@
# Guideline for Issues
- If you have any question about GitBucket, send it to [gitter room](https://gitter.im/takezoe/gitbucket) before raise an issue.
- 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/takezoe/gitbucket_ja).
- We can also support in Japaneses other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
- First priority of GitBucket is easy installation and reproduce GitHub behavior, so we might reject if your request is against it.

View File

@@ -1,8 +1,7 @@
GitBucket [![Gitter chat](https://badges.gitter.im/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.
GitBucket is a GitHub clone powered by Scala which has easy installation and high extensibility.
Features
--------
@@ -20,12 +19,12 @@ The current version of GitBucket provides a basic features below:
- Gravatar support
- Plug-in system
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/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.
@@ -40,9 +39,9 @@ or you can start GitBucket by `java -jar gitbucket.war` without servlet containe
- --host=[HOSTNAME]
- --gitbucket.home=[DATA_DIR]
To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/.gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk.
To upgrade GitBucket, only replace gitbucket.war after stop GitBucket. All GitBucket data is stored in HOME/.gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk.
For Installation on Windows Server with IIS see [this wiki page](https://github.com/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
@@ -65,7 +64,7 @@ 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
@@ -80,21 +79,44 @@ GitBucket has the plug-in system to extend GitBucket from outside of GitBucket.
- [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)
- [gitbucket-asciidoctor-plugin](https://github.com/lefou/gitbucket-asciidoctor-plugin)
You can find community plugins other than them at [gitbucket community plugins](http://gitbucket-plugins.github.io/).
Support
--------
- If you have any question about GitBucket, send it to [gitter room](https://gitter.im/takezoe/gitbucket) before raise an issue.
- 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/takezoe/gitbucket_ja).
- 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.10 - 30 Dec 2015
- Move to Bootstrap3
- New URL for raw contents (`raw/master/doc/activity.md` instead of `blob/master/doc/activity.md?raw=true`)
- Update xsbt-web-pligin
- Update H2 database
### 3.9 - 5 Dec 2015
- GFM inline breaks support in Markdown
- WebHook on create review comment is available
- WebHook event trigger is selectable
### 3.8 - 31 Oct 2015
- Moved to GitHub 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.

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

60
doc/authenticator.md Normal file
View File

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

View File

@@ -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 [gitbucket.core.servlet.AutoUpdate](https://github.com/takezoe/gitbucket/blob/master/src/main/scala/gitbucket/core/servlet/AutoUpdate.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.

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ Developer's Guide
* [How to run from source tree](how_to_run.md)
* [Directory Structure](directory.md)
* [Mapping and Validation](validation.md)
* Authentication in Controller (not yet)
* [Authentication in Controller](authenticator.md)
* [About Action in Issue Comment](comment_action.md)
* [Activity Types](activity.md)
* [Notification Email](notification.md)

View File

@@ -1,16 +1,15 @@
import sbt._
import Keys._
import org.scalatra.sbt._
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
import com.earldouglas.xwp.JettyPlugin
import play.twirl.sbt.SbtTwirl
import play.twirl.sbt.Import.TwirlKeys._
import sbtassembly._
import sbt.Keys._
import sbt._
import sbtassembly.AssemblyKeys._
import sbtassembly._
import JettyPlugin.autoImport._
object MyBuild extends Build {
val Organization = "gitbucket"
val Name = "gitbucket"
val Version = "3.7.0"
val Version = "3.10.1"
val ScalaVersion = "2.11.6"
val ScalatraVersion = "2.3.1"
@@ -18,7 +17,7 @@ object MyBuild extends Build {
"gitbucket",
file(".")
)
.settings(ScalatraPlugin.scalatraWithJRebel: _*)
// .settings(ScalatraPlugin.scalatraWithJRebel: _*)
.settings(
test in assembly := {},
assemblyMergeStrategy in assembly := {
@@ -49,32 +48,33 @@ object MyBuild extends Build {
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
"org.json4s" %% "json4s-jackson" % "3.2.11",
"jp.sf.amateras" %% "scalatra-forms" % "0.1.0",
"jp.sf.amateras" %% "scalatra-forms" % "0.2.0",
"commons-io" % "commons-io" % "2.4",
"io.github.gitbucket" % "markedj" % "1.0.3",
"io.github.gitbucket" % "markedj" % "1.0.6-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.sshd" % "apache-sshd" % "1.0.0",
"org.apache.tika" % "tika-core" % "1.10",
"com.typesafe.slick" %% "slick" % "2.1.0",
"com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.4.180",
// "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.16.v20140903" % "container;provided",
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"),
"com.h2database" % "h2" % "1.4.190",
"ch.qos.logback" % "logback-classic" % "1.1.1",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.16.v20140903" % "provided",
"javax.servlet" % "javax.servlet-api" % "3.0.1" % "provided",
"junit" % "junit" % "4.12" % "test",
"com.mchange" % "c3p0" % "0.9.5",
"com.mchange" % "c3p0" % "0.9.5.2",
"com.typesafe" % "config" % "1.2.1",
"com.typesafe.akka" %% "akka-actor" % "2.3.10",
"com.enragedginger" %% "akka-quartz-scheduler" % "1.3.0-akka-2.3.x" exclude("c3p0","c3p0")
),
play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._",
EclipseKeys.withSource := true,
javacOptions in compile ++= Seq("-target", "7", "-source", "7"),
javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml",
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),
javaOptions in Test += "-Dgitbucket.home=target/gitbucket_home_for_test",
testOptions in Test += Tests.Setup( () => new java.io.File("target/gitbucket_home_for_test").mkdir() ),
fork in Test := true,
packageOptions += Package.MainClass("JettyLauncher")
).enablePlugins(SbtTwirl)
).enablePlugins(SbtTwirl, JettyPlugin)
}

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,13 +20,21 @@ case class ApiCommit(
removed: List[String],
modified: List[String],
author: ApiPersonIdent,
committer: ApiPersonIdent)(repositoryName:RepositoryName) extends FieldSerializable{
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

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

View File

@@ -1,39 +0,0 @@
package gitbucket.core.api
import gitbucket.core.util.JGitUtil
import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util.RepositoryName
import org.eclipse.jgit.diff.DiffEntry
import org.eclipse.jgit.api.Git
import java.util.Date
/**
* https://developer.github.com/v3/activity/events/types/#pushevent
*/
case class ApiPushCommit(
id: String,
message: String,
timestamp: Date,
added: List[String],
removed: List[String],
modified: List[String],
author: ApiPersonIdent,
committer: ApiPersonIdent)(repositoryName:RepositoryName) extends FieldSerializable {
val url = ApiPath(s"/${repositoryName.fullName}/commit/${id}")
}
object ApiPushCommit{
def apply(commit: ApiCommit, repositoryName: RepositoryName): ApiPushCommit = ApiPushCommit(
id = commit.id,
message = commit.message,
timestamp = commit.timestamp,
added = commit.added,
removed = commit.removed,
modified = commit.modified,
author = commit.author,
committer = commit.committer)(repositoryName)
def apply(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiPushCommit =
ApiPushCommit(ApiCommit(git, repositoryName, commit), repositoryName)
}

View File

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

View File

@@ -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_count = watchers
val url = ApiPath(s"/api/v3/repos/${full_name}")
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

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

View File

@@ -90,8 +90,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
case class ForkRepositoryForm(owner: String, name: String)
val newRepositoryForm = mapping(
"owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
"name" -> trim(label("Repository name", text(required, maxlength(40), repository, uniqueRepository))),
"owner" -> trim(label("Owner" , text(required, maxlength(100), identifier, existsAccount))),
"name" -> trim(label("Repository name", text(required, maxlength(100), repository, uniqueRepository))),
"description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())),
"createReadme" -> trim(label("Create README" , boolean()))
@@ -212,6 +212,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
// // Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
// removeUserRelatedData(userName)
removeUserRelatedData(userName)
updateAccount(account.copy(isRemoved = true))
}
@@ -366,56 +367,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 +375,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 +496,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

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

@@ -104,7 +104,7 @@ trait IndexControllerBase extends ControllerBase {
})
/**
* JSON APU for checking user existence.
* JSON API for checking user existence.
*/
post("/_user/existence")(usersOnly {
getAccountByUserName(params("userName")).isDefined

View File

@@ -231,10 +231,20 @@ trait IssuesControllerBase extends ControllerBase {
} getOrElse {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("title" -> x.title,
"content" -> Markdown.toHtml(x.content getOrElse "No description given.",
repository, false, true, true, true, isEditable(x.userName, x.repositoryName, x.openedUserName))
))
Map(
"title" -> x.title,
"content" -> Markdown.toHtml(
markdown = x.content getOrElse "No description given.",
repository = repository,
enableWikiLink = false,
enableRefsLink = true,
enableAnchor = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = isEditable(x.userName, x.repositoryName, x.openedUserName)
)
)
)
}
} else Unauthorized
} getOrElse NotFound
@@ -249,9 +259,19 @@ trait IssuesControllerBase extends ControllerBase {
} getOrElse {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("content" -> view.Markdown.toHtml(x.content,
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName))
))
Map(
"content" -> view.Markdown.toHtml(
markdown = x.content,
repository = repository,
enableWikiLink = false,
enableRefsLink = true,
enableAnchor = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = isEditable(x.userName, x.repositoryName, x.commentedUserName)
)
)
)
}
} else Unauthorized
} getOrElse NotFound
@@ -332,6 +352,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
@@ -352,6 +373,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

View File

@@ -294,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
@@ -436,8 +439,11 @@ trait PullRequestsControllerBase extends ControllerBase {
// call web hook
callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get)
// notifications
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}")
}
@@ -447,6 +453,19 @@ trait PullRequestsControllerBase extends ControllerBase {
}
})
// TODO Same method exists in IssueController. Should it moved to IssueService?
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
StringUtil.extractIssueId(message).foreach { issueId =>
val content = fromIssue.issueId + ":" + fromIssue.title
if(getIssue(owner, repository, issueId).isDefined){
// Not add if refer comment already exist.
if(!getComments(owner, repository, issueId.toInt).exists { x => x.action == "refer" && x.content == content }) {
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, content, "refer")
}
}
}
}
/**
* Parses branch identifier and extracts owner and branch name as tuple.
*

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
@@ -28,7 +29,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean)
val optionsForm = mapping(
"repositoryName" -> trim(label("Description" , text(required, maxlength(40), identifier, renameRepositoryName))),
"repositoryName" -> trim(label("Repository Name", text(required, maxlength(40), identifier, renameRepositoryName))),
"description" -> trim(label("Description" , optional(text()))),
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
"isPrivate" -> trim(label("Repository Type", boolean()))
@@ -42,10 +43,11 @@ trait RepositorySettingsControllerBase extends ControllerBase {
)(CollaboratorForm.apply)
// for web hook url addition
case class WebHookForm(url: String)
case class WebHookForm(url: String, events: Set[WebHook.Event])
val webHookForm = mapping(
"url" -> trim(label("url", text(required, webHook)))
def webHookForm(update:Boolean) = mapping(
"url" -> trim(label("url", text(required, webHook(update)))),
"events" -> webhookEvents
)(WebHookForm.apply)
// for transfer ownership
@@ -138,14 +140,23 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Display the web hook page.
*/
get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
html.hooks(getWebHookURLs(repository.owner, repository.name), flash.get("url"), repository, flash.get("info"))
html.hooks(getWebHooks(repository.owner, repository.name), repository, flash.get("info"))
})
/**
* Display the web hook edit page.
*/
get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository =>
val webhook = WebHook(repository.owner, repository.name, "")
html.edithooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true)
})
/**
* Add the web hook URL.
*/
post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) =>
addWebHookURL(repository.owner, repository.name, form.url)
post("/:owner/:repository/settings/hooks/new", webHookForm(false))(ownerOnly { (form, repository) =>
addWebHook(repository.owner, repository.name, form.url, form.events)
flash += "info" -> s"Webhook ${form.url} created"
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
})
@@ -153,30 +164,87 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Delete the web hook URL.
*/
get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository =>
deleteWebHookURL(repository.owner, repository.name, params("url"))
deleteWebHook(repository.owner, repository.name, params("url"))
flash += "info" -> s"Webhook ${params("url")} deleted"
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
})
/**
* Send the test request to registered web hook URLs.
*/
post("/:owner/:repository/settings/hooks/test", webHookForm)(ownerOnly { (form, repository) =>
ajaxPost("/:owner/:repository/settings/hooks/test")(ownerOnly { repository =>
def _headers(h: Array[org.apache.http.Header]): Array[Array[String]] = h.map { h => Array(h.getName, h.getValue) }
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
import scala.collection.JavaConverters._
val commits = if(repository.commitCount == 0) List.empty else git.log
.add(git.getRepository.resolve(repository.repository.defaultBranch))
.setMaxCount(3)
.call.iterator.asScala.map(new CommitInfo(_))
import scala.concurrent.duration._
import scala.concurrent._
import scala.util.control.NonFatal
import org.apache.http.util.EntityUtils
import scala.concurrent.ExecutionContext.Implicits.global
getAccountByUserName(repository.owner).foreach { ownerAccount =>
callWebHook("push",
List(WebHook(repository.owner, repository.name, form.url)),
WebHookPushPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)
val url = params("url")
val dummyWebHookInfo = WebHook(repository.owner, repository.name, url)
val dummyPayload = {
val ownerAccount = getAccountByUserName(repository.owner).get
val commits = if(repository.commitCount == 0) List.empty else git.log
.add(git.getRepository.resolve(repository.repository.defaultBranch))
.setMaxCount(4)
.call.iterator.asScala.map(new CommitInfo(_)).toList
val pushedCommit = commits.drop(1)
WebHookPushPayload(
git = git,
sender = ownerAccount,
refName = "refs/heads/" + repository.repository.defaultBranch,
repositoryInfo = repository,
commits = pushedCommit,
repositoryOwner = ownerAccount,
oldId = commits.lastOption.map(_.id).map(ObjectId.fromString).getOrElse(ObjectId.zeroId()),
newId = commits.headOption.map(_.id).map(ObjectId.fromString).getOrElse(ObjectId.zeroId())
)
}
flash += "url" -> form.url
flash += "info" -> "Test payload deployed!"
val (webHook, json, reqFuture, resFuture) = callWebHook(WebHook.Push, List(dummyWebHookInfo), dummyPayload).head
val toErrorMap: PartialFunction[Throwable, Map[String,String]] = {
case e: java.net.UnknownHostException => Map("error"-> ("Unknown host " + e.getMessage))
case e: java.lang.IllegalArgumentException => Map("error"-> ("invalid url"))
case e: org.apache.http.client.ClientProtocolException => Map("error"-> ("invalid url"))
case NonFatal(e) => Map("error"-> (e.getClass + " "+ e.getMessage))
}
contentType = formats("json")
org.json4s.jackson.Serialization.write(Map(
"url" -> url,
"request" -> Await.result(reqFuture.map(req => Map(
"headers" -> _headers(req.getAllHeaders),
"payload" -> json
)).recover(toErrorMap), 20 seconds),
"responce" -> Await.result(resFuture.map(res => Map(
"status" -> res.getStatusLine(),
"body" -> EntityUtils.toString(res.getEntity()),
"headers" -> _headers(res.getAllHeaders())
)).recover(toErrorMap), 20 seconds)
))
}
})
/**
* Display the web hook edit page.
*/
get("/:owner/:repository/settings/hooks/edit")(ownerOnly { repository =>
getWebHook(repository.owner, repository.name, params("url")).map{ case (webhook, events) =>
html.edithooks(webhook, events, repository, flash.get("info"), false)
} getOrElse NotFound
})
/**
* Update web hook settings.
*/
post("/:owner/:repository/settings/hooks/edit", webHookForm(true))(ownerOnly { (form, repository) =>
updateWebHook(repository.owner, repository.name, form.url, form.events)
flash += "info" -> s"webhook ${form.url} updated"
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
})
@@ -226,9 +294,30 @@ trait RepositorySettingsControllerBase extends ControllerBase {
/**
* Provides duplication check for web hook url.
*/
private def webHook: Constraint = new Constraint(){
private def webHook(needExists: Boolean): Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.")
if(getWebHook(params("owner"), params("repository"), value).isDefined != needExists){
Some(if(needExists){
"URL had not been registered yet."
} else {
"URL had been registered already."
})
} else {
None
}
}
private def webhookEvents = new ValueType[Set[WebHook.Event]]{
def convert(name: String, params: Map[String, String], messages: Messages): Set[WebHook.Event] = {
WebHook.Event.values.flatMap { t =>
params.get(name + "." + t.name).map(_ => t)
}.toSet
}
def validate(name: String, params: Map[String, String], messages: Messages): Seq[(String, String)] = if(convert(name,params,messages).isEmpty){
Seq(name -> messages("error.required").format(name))
} else {
Nil
}
}
/**

View File

@@ -1,5 +1,7 @@
package gitbucket.core.controller
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import gitbucket.core.api._
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.repo.html
@@ -11,7 +13,7 @@ import gitbucket.core.util.StringUtil._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import gitbucket.core.model.{Account, CommitState}
import gitbucket.core.model.{Account, CommitState, WebHook}
import gitbucket.core.service.CommitStatusService
import gitbucket.core.service.WebHookService._
import gitbucket.core.view
@@ -32,7 +34,7 @@ import org.scalatra._
class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
with WebHookPullRequestService
with WebHookPullRequestService with WebHookPullRequestReviewCommentService
/**
* The repository viewer.
@@ -40,7 +42,7 @@ class RepositoryViewerController extends RepositoryViewerControllerBase
trait RepositoryViewerControllerBase extends ControllerBase {
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
with WebHookPullRequestService =>
with WebHookPullRequestService with WebHookPullRequestReviewCommentService =>
ArchiveCommand.registerFormat("zip", new ZipFormat)
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
@@ -102,11 +104,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
*/
post("/:owner/:repository/_preview")(referrersOnly { repository =>
contentType = "text/html"
helpers.markdown(params("content"), repository,
params("enableWikiLink").toBoolean,
params("enableRefsLink").toBoolean,
params("enableTaskList").toBoolean,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
helpers.markdown(
markdown = params("content"),
repository = repository,
enableWikiLink = params("enableWikiLink").toBoolean,
enableRefsLink = params("enableRefsLink").toBoolean,
enableLineBreaks = params("enableLineBreaks").toBoolean,
enableTaskList = params("enableTaskList").toBoolean,
hasWritePermission = hasWritePermission(repository.owner, repository.name, context.loginAccount)
)
})
/**
@@ -177,7 +183,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
*
* ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
*/
get("/api/v3/repos/:owner/:repo/commits/:ref/statuses")(referrersOnly { repository =>
val listStatusesRoute = get("/api/v3/repos/:owner/:repo/commits/:ref/statuses")(referrersOnly { repository =>
(for{
ref <- params.get("ref")
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
@@ -188,6 +194,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}) getOrElse NotFound
})
/**
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
*
* legacy route
*/
get("/api/v3/repos/:owner/:repo/statuses/:ref"){
listStatusesRoute.action()
}
/**
* https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
*
@@ -282,6 +297,21 @@ trait RepositoryViewerControllerBase extends ControllerBase {
redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}${if(form.path.length == 0) "" else form.path}")
})
get("/:owner/:repository/raw/*")(referrersOnly { repository =>
val (id, path) = splitPath(repository, multiParams("splat").head)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
getPathObjectId(git, path, revCommit).flatMap { objectId =>
JGitUtil.getObjectLoaderFromId(git, objectId){ loader =>
contentType = "application/octet-stream"
response.setContentLength(loader.getSize.toInt)
loader.copyTo(response.outputStream)
()
}
} getOrElse NotFound
}
})
/**
* Displays the file content of the specified branch or commit.
*/
@@ -292,9 +322,12 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
getPathObjectId(git, path, revCommit).map { objectId =>
if(raw){
// Download
JGitUtil.getContentFromId(git, objectId, true).map { bytes =>
RawData("application/octet-stream", bytes)
// Download (This route is left for backword compatibility)
JGitUtil.getObjectLoaderFromId(git, objectId){ loader =>
contentType = "application/octet-stream"
response.setContentLength(loader.getSize.toInt)
loader.copyTo(response.outputStream)
()
} getOrElse NotFound
} else {
html.blob(id, repository, path.split("/").toList,
@@ -366,7 +399,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
post("/:owner/:repository/commit/:id/comment/new", commentForm)(readableUsersOnly { (form, repository) =>
val id = params("id")
createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName, form.content,
form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId.isDefined)
form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId)
form.issueId match {
case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
@@ -391,13 +424,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
ajaxPost("/:owner/:repository/commit/:id/comment/_data/new", commentForm)(readableUsersOnly { (form, repository) =>
val id = params("id")
val commentId = createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName,
form.content, form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId.isDefined)
form.content, form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId)
val comment = getCommitComment(repository.owner, repository.name, commentId.toString).get
form.issueId match {
case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
case Some(issueId) =>
recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
callPullRequestReviewCommentWebHook("create", comment, repository, issueId, context.baseUrl, context.loginAccount.get)
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
}
helper.html.commitcomment(getCommitComment(repository.owner, repository.name, commentId.toString).get,
hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
helper.html.commitcomment(comment, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
})
ajaxGet("/:owner/:repository/commit_comments/_data/:id")(readableUsersOnly { repository =>
@@ -409,8 +444,16 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} getOrElse {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("content" -> view.Markdown.toHtml(x.content,
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName))
Map(
"content" -> view.Markdown.toHtml(
markdown = x.content,
repository = repository,
enableWikiLink = false,
enableRefsLink = true,
enableAnchor = true,
enableLineBreaks = true,
hasWritePermission = isEditable(x.userName, x.repositoryName, x.commentedUserName)
)
))
}
} else Unauthorized
@@ -663,9 +706,10 @@ trait RepositoryViewerControllerBase extends ControllerBase {
// call web hook
callPullRequestWebHookByRequestBranch("synchronize", repository, branch, context.baseUrl, loginAccount)
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
callWebHookOf(repository.owner, repository.name, "push") {
callWebHookOf(repository.owner, repository.name, WebHook.Push) {
getAccountByUserName(repository.owner).map{ ownerAccount =>
WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount)
WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount,
oldId = headTip, newId = commitId)
}
}
}
@@ -717,4 +761,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
override protected def renderUncaughtException(e: Throwable)(implicit request: HttpServletRequest, response: HttpServletResponse): Unit = {
e.printStackTrace()
}
}

View File

@@ -100,12 +100,12 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
if(form.isRemoved){
// Remove repositories
getRepositoryNamesOfUser(userName).foreach { repositoryName =>
deleteRepository(userName, repositoryName)
FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
}
// getRepositoryNamesOfUser(userName).foreach { repositoryName =>
// deleteRepository(userName, repositoryName)
// FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
// }
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
removeUserRelatedData(userName)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -92,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)]] {

View File

@@ -47,6 +47,7 @@ trait RepositoryService { self: AccountService =>
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val webHookEvents = WebHookEvents .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list
@@ -66,10 +67,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 =>
@@ -79,9 +76,10 @@ trait RepositoryService { self: AccountService =>
deleteRepository(oldUserName, oldRepositoryName)
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Milestones.insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
WebHookEvents.insertAll(webHookEvents .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list
Issues.insertAll(issues.map { x => x.copy(
@@ -98,6 +96,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
@@ -145,6 +148,7 @@ trait RepositoryService { self: AccountService =>
IssueId .filter(_.byRepository(userName, repositoryName)).delete
Milestones .filter(_.byRepository(userName, repositoryName)).delete
WebHooks .filter(_.byRepository(userName, repositoryName)).delete
WebHookEvents .filter(_.byRepository(userName, repositoryName)).delete
Repositories .filter(_.byRepository(userName, repositoryName)).delete
// Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME

View File

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

View File

@@ -21,6 +21,9 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
new Version(3, 10),
new Version(3, 9),
new Version(3, 8),
new Version(3, 7) with SystemSettingsService {
override def update(conn: Connection, cl: ClassLoader): Unit = {
super.update(conn, cl)

View File

@@ -3,7 +3,7 @@ package gitbucket.core.servlet
import java.io.File
import gitbucket.core.api
import gitbucket.core.model.Session
import gitbucket.core.model.{Session, WebHook}
import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry}
import gitbucket.core.service.IssuesService.IssueSearchCondition
import gitbucket.core.service.WebHookService._
@@ -200,10 +200,11 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
}
// call web hook
callWebHookOf(owner, repository, "push"){
callWebHookOf(owner, repository, WebHook.Push){
for(pusherAccount <- getAccountByUserName(pusher);
ownerAccount <- getAccountByUserName(owner)) yield {
WebHookPushPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount)
WebHookPushPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount,
newId = command.getNewId(), oldId = command.getOldId())
}
}
}

View File

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

View File

@@ -1,20 +1,25 @@
package gitbucket.core.ssh
import java.io.File
import java.util.concurrent.atomic.AtomicBoolean
import javax.servlet.{ServletContextEvent, ServletContextListener}
import gitbucket.core.service.SystemSettingsService
import gitbucket.core.util.Directory
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider
import org.slf4j.LoggerFactory
import java.util.concurrent.atomic.AtomicBoolean
object SshServer {
private val logger = LoggerFactory.getLogger(SshServer.getClass)
private val server = org.apache.sshd.SshServer.setUpDefaultServer()
private val server = org.apache.sshd.server.SshServer.setUpDefaultServer()
private val active = new AtomicBoolean(false)
private def configure(port: Int, baseUrl: String) = {
server.setPort(port)
server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(s"${Directory.GitBucketHome}/gitbucket.ser"))
val provider = new SimpleGeneratorHostKeyProvider(new File(s"${Directory.GitBucketHome}/gitbucket.ser"))
provider.setAlgorithm("RSA")
provider.setOverwriteAllowed(false)
server.setKeyPairProvider(provider)
server.setPublickeyAuthenticator(new PublicKeyAuthenticator)
server.setCommandFactory(new GitCommandFactory(baseUrl))
server.setShellFactory(new NoShell)

View File

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

View File

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

@@ -75,7 +75,14 @@ class Mailer(private val smtp: Smtp) extends Notifier {
database withSession { implicit session =>
defining(
s"[${r.name}] ${issue.title} (#${issue.issueId})" ->
msg(Markdown.toHtml(content, r, false, true, false))) { case (subject, msg) =>
msg(Markdown.toHtml(
markdown = content,
repository = r,
enableWikiLink = false,
enableRefsLink = true,
enableAnchor = false,
enableLineBreaks = false
))) { case (subject, msg) =>
recipients(issue) { to =>
val email = new HtmlEmail
email.setHostName(smtp.host)

View File

@@ -7,13 +7,31 @@ 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,
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 username/project@SHA to link
@@ -26,10 +44,12 @@ trait LinkConverter { self: RequestCache =>
// 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)}""")
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)}""")
}
}
@@ -43,10 +63,12 @@ trait LinkConverter { self: RequestCache =>
// 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)}""")
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)}""")
}
}
@@ -54,10 +76,12 @@ trait LinkConverter { self: RequestCache =>
.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)}""")
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)}""")
}
}

View File

@@ -19,6 +19,7 @@ object Markdown {
* @param enableWikiLink if true then wiki style link is available in 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 enableLineBreaks if true then render line breaks as &lt;br&gt;
* @param enableTaskList if true then task list syntax is available
* @param hasWritePermission true if user has writable to ths given repository
* @param pages the list of existing Wiki pages
@@ -28,6 +29,7 @@ object Markdown {
enableWikiLink: Boolean,
enableRefsLink: Boolean,
enableAnchor: Boolean,
enableLineBreaks: Boolean,
enableTaskList: Boolean = false,
hasWritePermission: Boolean = false,
pages: List[String] = Nil)(implicit context: Context): String = {
@@ -43,6 +45,7 @@ object Markdown {
val options = new Options()
options.setSanitize(true)
options.setBreaks(enableLineBreaks)
val renderer = new GitBucketMarkedRenderer(options, repository, enableWikiLink, enableRefsLink, enableAnchor, enableTaskList, hasWritePermission, pages)
Marked.marked(source, options, renderer)
}
@@ -110,7 +113,7 @@ object Markdown {
}
override def link(href: String, title: String, text: String): String = {
super.link(fixUrl(href, true), title, text)
super.link(fixUrl(href, false), title, text)
}
override def image(href: String, title: String, text: String): String = {

View File

@@ -84,15 +84,30 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
/**
* Converts Markdown of Wiki pages to HTML.
*/
def markdown(value: String,
def markdown(markdown: String,
repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
enableRefsLink: Boolean,
enableLineBreaks: Boolean,
enableTaskList: Boolean = false,
hasWritePermission: Boolean = false,
pages: List[String] = Nil)(implicit context: Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, true, enableTaskList, hasWritePermission, pages))
Html(Markdown.toHtml(
markdown = markdown,
repository = repository,
enableWikiLink = enableWikiLink,
enableRefsLink = enableRefsLink,
enableAnchor = true,
enableLineBreaks = enableLineBreaks,
enableTaskList = enableTaskList,
hasWritePermission = hasWritePermission,
pages = 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 +118,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 +300,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

@@ -5,17 +5,17 @@
@import gitbucket.core.view.helpers._
@html.main("Applications"){
<div class="container">
<div class="row-fluid">
<div class="span3">
<div class="row">
<div class="col-md-3">
@menu("application", settings.ssh)
</div>
<div class="span9">
<div class="box">
<div class="box-header">Personal access tokens</div>
<div class="box-content">
<div class="col-md-9">
<div class="panel panel-default">
<div class="panel-heading strong">Personal access tokens</div>
<div class="panel-body">
@if(personalTokens.isEmpty && gneratedToken.isEmpty){
No tokens.
}else{
} else {
Tokens you have generated that can be used to access the GitBucket API.<hr>
}
@gneratedToken.map{ case (token, tokenString) =>
@@ -38,13 +38,13 @@
</div>
</div>
<form method="POST" action="@path/@account.userName/_personalToken" validate="true">
<div class="box">
<div class="box-header">Generate new token</div>
<div class="box-content">
<div class="panel panel-default">
<div class="panel-heading strong">Generate new token</div>
<div class="panel-body">
<fieldset>
<label for="note" class="strong">Token description</label>
<div><span id="error-note" class="error"></span></div>
<input type="text" name="note" id="note" style="width: 400px;"/>
<input type="text" name="note" id="note" class="form-control" style="width: 400px;"/>
<p class="muted">What's this token for?</p>
</fieldset>
<input type="submit" class="btn btn-success" value="Generate token"/>

View File

@@ -4,46 +4,46 @@
@import gitbucket.core.view.helpers._
@html.main("Edit your profile"){
<div class="container">
<div class="row-fluid">
<div class="span3">
<div class="row">
<div class="col-md-3">
@menu("profile", settings.ssh)
</div>
<div class="span9">
<div class="col-md-9">
@helper.html.information(info)
@if(LDAPUtil.isDummyMailAddress(account)){<div class="alert alert-danger">Please register your mail address.</div>}
<form action="@url(account.userName)/_edit" method="POST" validate="true">
<div class="box">
<div class="box-header">Profile</div>
<div class="box-content">
<div class="row-fluid">
<div class="span6">
<div class="panel panel-default">
<div class="panel-heading strong">Profile</div>
<div class="panel-body">
<div class="row">
<div class="col-md-6">
@if(account.password.nonEmpty){
<fieldset>
<fieldset class="form-group">
<label for="password" class="strong">
Password (input to change password):
</label>
<input type="password" name="password" id="password" value="" autocomplete="off"/>
<input type="password" name="password" id="password" class="form-control" value="" autocomplete="off"/>
<span id="error-password" class="error"></span>
</fieldset>
}
<fieldset>
<fieldset class="form-group">
<label for="fullName" class="strong">Full Name:</label>
<input type="text" name="fullName" id="fullName" value="@account.fullName"/>
<input type="text" name="fullName" id="fullName" class="form-control" value="@account.fullName"/>
<span id="error-fullName" class="error"></span>
</fieldset>
<fieldset>
<fieldset class="form-group">
<label for="mailAddress" class="strong">Mail Address:</label>
<input type="text" name="mailAddress" id="mailAddress" value="@if(!LDAPUtil.isDummyMailAddress(account)){@account.mailAddress}"/>
<input type="text" name="mailAddress" id="mailAddress" class="form-control" value="@if(!LDAPUtil.isDummyMailAddress(account)){@account.mailAddress}"/>
<span id="error-mailAddress" class="error"></span>
</fieldset>
<fieldset>
<fieldset class="form-group">
<label for="url" class="strong">URL (optional):</label>
<input type="text" name="url" id="url" style="width: 300px;" value="@account.url"/>
<input type="text" name="url" id="url" class="form-control" value="@account.url"/>
<span id="error-url" class="error"></span>
</fieldset>
</div>
<div class="span6">
<fieldset>
<div class="col-md-6">
<fieldset class="form-group">
<label for="avatar" class="strong">Image (optional):</label>
@helper.html.uploadavatar(Some(account))
</fieldset>
@@ -54,7 +54,7 @@
<a href="@path/@account.userName/_delete" class="btn btn-danger" id="delete">Delete account</a>
</div>
<input type="submit" class="btn btn-success" value="Save"/>
@if(!LDAPUtil.isDummyMailAddress(account)){<a href="@url(account.userName)" class="btn">Cancel</a>}
@if(!LDAPUtil.isDummyMailAddress(account)){<a href="@url(account.userName)" class="btn btn-default">Cancel</a>}
</div>
</div>
</div>

View File

@@ -4,34 +4,34 @@
@html.main(if(account.isEmpty) "Create group" else "Edit group"){
<div class="container">
<form id="form" method="post" action="@if(account.isEmpty){@path/groups/new} else {@path/@account.get.userName/_editgroup}" validate="true">
<div class="row-fluid">
<div class="span5">
<fieldset>
<div class="row">
<div class="col-md-5">
<fieldset class="form-group">
<label for="groupName" class="strong">Group name</label>
<div>
<span id="error-groupName" class="error"></span>
</div>
<input type="text" name="groupName" id="groupName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
<input type="text" name="groupName" id="groupName" class="form-control" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
</fieldset>
<fieldset>
<fieldset class="form-group">
<label class="strong">URL (Optional)</label>
<div>
<span id="error-url" class="error"></span>
</div>
<input type="text" name="url" id="url" style="width: 300px;" value="@account.map(_.url)"/>
<input type="text" name="url" id="url" class="form-control" value="@account.map(_.url)"/>
</fieldset>
<fieldset>
<fieldset class="form-group">
<label for="avatar" class="strong">Image (Optional)</label>
@helper.html.uploadavatar(account)
</fieldset>
</div>
<div class="span7">
<fieldset>
<div class="col-md-7">
<fieldset class="form-group">
<label class="strong">Members</label>
<ul id="member-list" class="collaborator">
</ul>
@helper.html.account("memberName", 200)
<input type="button" class="btn" value="Add" id="addMember"/>
<input type="button" class="btn btn-default" value="Add" id="addMember"/>
<input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.isManager).mkString(",")"/>
<div>
<span class="error" id="error-members"></span>
@@ -42,12 +42,12 @@
<fieldset class="margin">
@if(account.isDefined){
<div class="pull-right">
<a href="@url(account.get.userName)/_deletegroup" id="delete" class="btn btn-danger">Delete Group</a>
<a href="@url(account.get.userName)/_deletegroup" id="delete" class="btn btn-danger btn-lg">Delete Group</a>
</div>
}
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create Group} else {Update Group}"/>
<input type="submit" class="btn btn-success btn-lg" value="@if(account.isEmpty){Create Group} else {Update Group}"/>
@if(account.isDefined){
<a href="@url(account.get.userName)" class="btn">Cancel</a>
<a href="@url(account.get.userName)" class="btn btn-default btn-lg">Cancel</a>
}
</fieldset>
</form>

View File

@@ -5,8 +5,8 @@
@html.main(account.userName){
<div class="container">
<div class="container-fluid">
<div class="row-fluid">
<div class="span4">
<div class="row">
<div class="col-md-4">
<div class="block">
<div class="account-image">@avatar(account.userName, 270)</div>
<div class="account-fullname">@account.fullName</div>
@@ -28,7 +28,7 @@
}
</div>
<div class="span8">
<div class="col-md-8">
<ul class="nav nav-tabs" style="margin-bottom: 5px;">
<li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
@if(account.isGroupAccount){
@@ -39,14 +39,14 @@
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
<li class="pull-right">
<div class="button-group">
<a href="@url(account.userName)/_edit" class="btn">Edit Your Profile</a>
<a href="@url(account.userName)/_edit" class="btn btn-default">Edit Your Profile</a>
</div>
</li>
}
@if(loginAccount.isDefined && account.isGroupAccount && isGroupManager){
<li class="pull-right">
<div class="button-group">
<a href="@url(account.userName)/_editgroup" class="btn">Edit Group</a>
<a href="@url(account.userName)/_editgroup" class="btn btn-default">Edit Group</a>
</div>
</li>
}

View File

@@ -4,45 +4,56 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
@import gitbucket.core.view.helpers._
@html.main("Create a New Repository"){
<div style="width: 600px; margin: 10px auto;">
<h2>Create a new repository</h2>
<p class="muted">
A repository contains all the files for your project, including the revision history.
</p>
<form id="form" method="post" action="@path/new" validate="true">
<fieldset>
<label for="name" class="strong">Repository name:</label>
<div class="btn-group" style="margin-bottom: 10px;" id="owner-dropdown">
<button class="btn dropdown-toggle" data-toggle="dropdown">
<span class="strong">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<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>
}
</ul>
<input type="hidden" name="owner" id="owner" value="@loginAccount.get.userName"/>
</div>
<span class="slash">/</span>
<input type="text" name="name" id="name" autofocus />
<span id="error-name" class="error"></span>
<fieldset class="margin form-group">
<dl style="float: left;">
<dt>Owner</dt>
<dd style="margin-left: 0px;">
<div class="btn-group" id="owner-dropdown">
<button class="btn dropdown-toggle btn-default" data-toggle="dropdown">
<span class="strong">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<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="octicon"></i> <span>@avatar(groupName, 20) @groupName</span></a></li>
}
</ul>
<input type="hidden" name="owner" id="owner" value="@loginAccount.get.userName"/>
</div>
</dd>
</dl>
<span class="slash" style="float: left; margin-left: 10px; margin-right: 10px; margin-top: 15px;">/</span>
<dl>
<dt>Repository name</dt>
<dd style="margin-left: 0px;">
<input type="text" name="name" id="name" class="form-control" style="width: 200px;" autofocus />
<span id="error-name" class="error"></span>
</dd>
</dl>
</fieldset>
<fieldset>
<fieldset class="form-group">
<label for="description" class="strong">Description (optional):</label>
<input type="text" name="description" id="description" style="width: 95%;"/>
<input type="text" name="description" id="description" class="form-control" style="width: 95%;"/>
</fieldset>
<fieldset class="margin">
<label class="radio">
<input type="radio" name="isPrivate" value="false" @if(isCreateRepoOptionPublic){checked}>
<span class="strong"><i class="octicon octicon-repo"></i>&nbsp;</i>&nbsp;Public</span><br>
<div>
<span>All users and guests can read this repository.</span>
<span class="strong"><i class="octicon octicon-repo"></i>&nbsp;</i>&nbsp;Public</span>
<div class="normal muted">
Anyone can see this repository. You choose who can commit.
</div>
</label>
</fieldset>
<fieldset>
<label class="radio">
<input type="radio" name="isPrivate" value="true" @if(!isCreateRepoOptionPublic){checked}>
<span class="strong"><i class="octicon octicon-lock"></i>&nbsp;</i>&nbsp;Private</span><br>
<div>
<span>Only collaborators can read this repository.</span>
<span class="strong"><i class="octicon octicon-lock"></i>&nbsp;</i>&nbsp;Private</span>
<div class="normal muted">
You choose who can see and commit to this repository.
</div>
</label>
</fieldset>
@@ -50,13 +61,13 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
<label for="createReadme" class="checkbox">
<input type="checkbox" name="createReadme" id="createReadme"/>
<span class="strong">Initialize this repository with a README</span>
<div>
<span>This will allow you to <code>git clone</code> the repository immediately.</span>
<div class="normal muted">
This will let you immediately clone the repository to your computer. Skip this step if youre importing an existing repository.
</div>
</label>
</fieldset>
<fieldset class="margin">
<input type="submit" class="btn btn-success" value="Create repository"/>
<fieldset class="margin form-actions">
<input type="submit" class="btn btn-success btn-lg" value="Create repository"/>
</fieldset>
</form>
</div>
@@ -66,8 +77,8 @@ $('#owner-dropdown a').click(function(){
var userName = $(this).data('name');
$('#owner').val(userName);
$('#owner-dropdown i').attr('class', 'icon-white');
$(this).find('i').attr('class', 'icon-ok');
$('#owner-dropdown i').attr('class', 'octicon');
$(this).find('i').attr('class', 'octicon octicon-check');
$('#owner-dropdown span.strong').html($(this).find('span').html());
});

View File

@@ -5,8 +5,8 @@
<div class="container">
<h3>Create your account</h3>
<form action="@path/register" method="POST" validate="true">
<div class="row-fluid">
<div class="span6">
<div class="row">
<div class="col-md-6">
<fieldset>
<label for="userName" class="strong">Username:</label>
<input type="text" name="userName" id="userName" value="" autofocus/>
@@ -35,7 +35,7 @@
<span id="error-url" class="error"></span>
</fieldset>
</div>
<div class="span6">
<div class="col-md-6">
<fieldset>
<label for="avatar" class="strong">Image (optional):</label>
@helper.html.uploadavatar(None)

View File

@@ -4,14 +4,14 @@
@import gitbucket.core.view.helpers._
@html.main("SSH Keys"){
<div class="container">
<div class="row-fluid">
<div class="span3">
<div class="row">
<div class="col-md-3">
@menu("ssh", settings.ssh)
</div>
<div class="span9">
<div class="box">
<div class="box-header">SSH Keys</div>
<div class="box-content">
<div class="col-md-9">
<div class="panel panel-default">
<div class="panel-heading strong">SSH Keys</div>
<div class="panel-body">
@if(sshKeys.isEmpty){
No keys
}
@@ -25,18 +25,18 @@
</div>
</div>
<form method="POST" action="@path/@account.userName/_ssh" validate="true">
<div class="box">
<div class="box-header">Add an SSH Key</div>
<div class="box-content">
<fieldset>
<div class="panel panel-default">
<div class="panel-heading strong">Add an SSH Key</div>
<div class="panel-body">
<fieldset class="form-group">
<label for="title" class="strong">Title</label>
<div><span id="error-title" class="error"></span></div>
<input type="text" name="title" id="title" style="width: 400px;"/>
<input type="text" name="title" id="title" class="form-control" style="width: 400px;"/>
</fieldset>
<fieldset>
<fieldset class="form-group">
<label for="publicKey" class="strong">Key</label>
<div><span id="error-publicKey" class="error"></span></div>
<textarea name="publicKey" id="publicKey" style="width: 600px; height: 250px;"></textarea>
<textarea name="publicKey" id="publicKey" class="form-control" style="width: 600px; height: 250px;"></textarea>
</fieldset>
<input type="submit" class="btn btn-success" value="Add"/>
</div>

View File

@@ -1,8 +1,8 @@
@(active: String)(body: Html)(implicit context: gitbucket.core.controller.Context)
@import context._
<div class="container">
<div class="row-fluid">
<div class="span3">
<div class="container body">
<div class="row">
<div class="col-md-3">
<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>
@@ -18,7 +18,7 @@
</li>
</ul>
</div>
<div class="span9">
<div class="col-md-9">
@body
</div>
</div>

View File

@@ -2,29 +2,41 @@
@import context._
@import gitbucket.core.view.helpers._
@html.main("Plugins"){
@admin.html.menu("plugins") {
<h1>Installed plugins</h1>
@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>
@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>
}
@plugins.map {plugin =>
<div class="panel panel-default">
<div class="panel-heading strong">@plugin.pluginName</div>
<div class="panel-body">
<div class="row">
<label class="col-md-2">Id</label>
<span class="col-md-8">@plugin.pluginId</span>
</div>
<div class="row">
<label class="col-md-2">Version</label>
<span class="col-md-8">@plugin.version</span>
</div>
<div class="row">
<label class="col-md-2">Name</label>
<span class="col-md-8">@plugin.pluginName</span>
</div>
<div class="row">
<label class="col-md-2">Description</label>
<span class="col-md-8 muted">@plugin.description</span>
</div>
</div>
</div>
}
} else {
<p>No plugin detected on your gitbucket installation.</p>
}
}
}

View File

@@ -5,10 +5,10 @@
@html.main("System Settings"){
@menu("system"){
@helper.html.information(info)
<form action="@path/admin/system" method="POST" validate="true">
<div class="box">
<div class="box-header">System Settings</div>
<div class="box-content">
<form action="@path/admin/system" method="POST" validate="true" class="form-horizontal">
<div class="panel panel-default">
<div class="panel-heading strong">System Settings</div>
<div class="panel-body">
<!--====================================================================-->
<!-- GITBUCKET_HOME -->
<!--====================================================================-->
@@ -21,7 +21,7 @@
<label><span class="strong">Base URL</span> (e.g. <code>http://example.com/gitbucket</code>)</label>
<fieldset>
<div class="controls">
<input type="text" name="baseUrl" id="baseUrl" style="width: 400px" value="@settings.baseUrl"/>
<input type="text" name="baseUrl" id="baseUrl" class="form-control" value="@settings.baseUrl"/>
<span id="error-baseUrl" class="error"></span>
</div>
</fieldset>
@@ -36,7 +36,7 @@
<hr>
<label><span class="strong">Information</span> (HTML is available)</label>
<fieldset>
<textarea name="information" style="width: 600px; height: 100px;">@settings.information</textarea>
<textarea name="information" class="form-control" style="height: 100px;">@settings.information</textarea>
</fieldset>
<!--====================================================================-->
<!-- Account registration -->
@@ -46,11 +46,11 @@
<fieldset>
<label class="radio">
<input type="radio" name="allowAccountRegistration" value="true"@if(settings.allowAccountRegistration){ checked}>
<span class="strong">Allow</span> - Users can create accounts by themselves.
<span class="strong">Allow</span> <span class="normal">- Users can create accounts by themselves.</span>
</label>
<label class="radio">
<input type="radio" name="allowAccountRegistration" value="false"@if(!settings.allowAccountRegistration){ checked}>
<span class="strong">Deny</span> - Only administrators can create accounts.
<span class="strong">Deny</span> - <span class="normal">Only administrators can create accounts.</span>
</label>
</fieldset>
<hr>
@@ -58,11 +58,11 @@
<fieldset>
<label class="radio">
<input type="radio" name="isCreateRepoOptionPublic" value="true"@if(settings.isCreateRepoOptionPublic){ checked}>
<span class="strong">Public</span> - All users and guests can read that repository.
<span class="strong">Public</span> <span class="normal">- All users and guests can read that repository.</span>
</label>
<label class="radio">
<input type="radio" name="isCreateRepoOptionPublic" value="false"@if(!settings.isCreateRepoOptionPublic){ checked}>
<span class="strong">Private</span> - Only collaborators can read that repository.
<span class="strong">Private</span> <span class="normal">- Only collaborators can read that repository.</span>
</label>
</fieldset>
<!--====================================================================-->
@@ -73,11 +73,11 @@
<fieldset>
<label class="radio">
<input type="radio" name="allowAnonymousAccess" value="true"@if(settings.allowAnonymousAccess){ checked}>
<span class="strong">Allow</span> - Anyone can view public repositories, user/group profiles.
<span class="strong">Allow</span> <span class="normal">- Anyone can view public repositories, user/group profiles.</span>
</label>
<label class="radio">
<input type="radio" name="allowAnonymousAccess" value="false"@if(!settings.allowAnonymousAccess){ checked}>
<span class="strong">Deny</span> - Users must authenticate before viewing any information
<span class="strong">Deny</span> <span class="normal">- Users must authenticate before viewing any information.</span>
</label>
</fieldset>
<!--====================================================================-->
@@ -86,7 +86,7 @@
<hr>
<label><span class="strong">Limit of activity logs</span> (Unlimited if it's not specified or zero)</label>
<div class="controls">
<input type="text" id="activityLogLimit" name="activityLogLimit" class="input-mini" value="@settings.activityLogLimit"/>
<input type="text" id="activityLogLimit" name="activityLogLimit" class="form-control input-mini" value="@settings.activityLogLimit"/>
<span id="error-activityLogLimit" class="error"></span>
</div>
<!--====================================================================-->
@@ -111,13 +111,11 @@
Enable SSH access to git repository
</label>
</fieldset>
<div class="form-horizontal ssh">
<div class="control-group">
<label class="control-label" for="sshPort">SSH Port</label>
<div class="controls">
<input type="text" id="sshPort" name="sshPort" value="@settings.sshPort"/>
<span id="error-sshPort" class="error"></span>
</div>
<div class="form-group ssh">
<label class="control-label col-md-3" for="sshPort">SSH Port</label>
<div class="col-md-9">
<input type="text" id="sshPort" name="sshPort" class="form-control" value="@settings.sshPort"/>
<span id="error-sshPort" class="error"></span>
</div>
</div>
<p class="muted">
@@ -130,92 +128,90 @@
<label class="strong">Authentication</label>
<fieldset>
<label class="checkbox">
<input type="checkbox" id="ldapAuthentication" name="ldapAuthentication"@if(settings.ldap){ checked}/>
<input type="checkbox" id="ldapAuthentication" name="ldapAuthentication"@if(settings.ldap){ checked} />
LDAP
</label>
</fieldset>
<div class="form-horizontal ldap">
<div class="control-group">
<label class="control-label" for="ldapHost">LDAP Host</label>
<div class="controls">
<input type="text" id="ldapHost" name="ldap.host" value="@settings.ldap.map(_.host)"/>
<div class="ldap">
<div class="form-group">
<label class="control-label col-md-3" for="ldapHost">LDAP Host</label>
<div class="col-md-9">
<input type="text" id="ldapHost" name="ldap.host" class="form-control" value="@settings.ldap.map(_.host)"/>
<span id="error-ldap_host" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapPort">LDAP Port</label>
<div class="controls">
<input type="text" id="ldapPort" name="ldap.port" class="input-mini" value="@settings.ldap.map(_.port)"/>
<div class="form-group">
<label class="control-label col-md-3" for="ldapPort">LDAP Port</label>
<div class="col-md-9">
<input type="text" id="ldapPort" name="ldap.port" class="form-control input-mini" value="@settings.ldap.map(_.port)"/>
<span id="error-ldap_port" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapBindDN">Bind DN</label>
<div class="controls">
<input type="text" id="ldapBindDN" name="ldap.bindDN" value="@settings.ldap.map(_.bindDN)"/>
<div class="form-group">
<label class="control-label col-md-3" for="ldapBindDN">Bind DN</label>
<div class="col-md-9">
<input type="text" id="ldapBindDN" name="ldap.bindDN" class="form-control" value="@settings.ldap.map(_.bindDN)"/>
<span id="error-ldap_bindDN" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapBindPassword">Bind Password</label>
<div class="controls">
<input type="password" id="ldapBindPassword" name="ldap.bindPassword" value="@settings.ldap.map(_.bindPassword)"/>
<div class="form-group">
<label class="control-label col-md-3" for="ldapBindPassword">Bind Password</label>
<div class="col-md-9">
<input type="password" id="ldapBindPassword" name="ldap.bindPassword" class="form-control" value="@settings.ldap.map(_.bindPassword)"/>
<span id="error-ldap_bindPassword" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapBaseDN">Base DN</label>
<div class="controls">
<input type="text" id="ldapBaseDN" name="ldap.baseDN" value="@settings.ldap.map(_.baseDN)"/>
<div class="form-group">
<label class="control-label col-md-3" for="ldapBaseDN">Base DN</label>
<div class="col-md-9">
<input type="text" id="ldapBaseDN" name="ldap.baseDN" class="form-control" value="@settings.ldap.map(_.baseDN)"/>
<span id="error-ldap_baseDN" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapUserNameAttribute">User name attribute</label>
<div class="controls">
<input type="text" id="ldapUserNameAttribute" name="ldap.userNameAttribute" value="@settings.ldap.map(_.userNameAttribute)"/>
<div class="form-group">
<label class="control-label col-md-3" for="ldapUserNameAttribute">User name attribute</label>
<div class="col-md-9">
<input type="text" id="ldapUserNameAttribute" name="ldap.userNameAttribute" class="form-control" value="@settings.ldap.map(_.userNameAttribute)"/>
<span id="error-ldap_userNameAttribute" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapAdditionalFilterCondition">Additional filter condition</label>
<div class="controls">
<input type="text" id="ldapAdditionalFilterCondition" name="ldap.additionalFilterCondition" value="@settings.ldap.map(_.additionalFilterCondition)"/>
<div class="form-group">
<label class="control-label col-md-3" for="ldapAdditionalFilterCondition">Additional filter condition</label>
<div class="col-md-9">
<input type="text" id="ldapAdditionalFilterCondition" name="ldap.additionalFilterCondition" class="form-control" value="@settings.ldap.map(_.additionalFilterCondition)"/>
<span id="error-ldap_additionalFilterCondition" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapFullNameAttribute">Full name attribute</label>
<div class="controls">
<input type="text" id="ldapFullNameAttribute" name="ldap.fullNameAttribute" value="@settings.ldap.map(_.fullNameAttribute)"/>
<div class="form-group">
<label class="control-label col-md-3" for="ldapFullNameAttribute">Full name attribute</label>
<div class="col-md-9">
<input type="text" id="ldapFullNameAttribute" name="ldap.fullNameAttribute" class="form-control" value="@settings.ldap.map(_.fullNameAttribute)"/>
<span id="error-ldap_fullNameAttribute" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapMailAttribute">Mail address attribute</label>
<div class="controls">
<input type="text" id="ldapMailAttribute" name="ldap.mailAttribute" value="@settings.ldap.map(_.mailAttribute)"/>
<div class="form-group">
<label class="control-label col-md-3" for="ldapMailAttribute">Mail address attribute</label>
<div class="col-md-9">
<input type="text" id="ldapMailAttribute" name="ldap.mailAttribute" class="form-control" value="@settings.ldap.map(_.mailAttribute)"/>
<span id="error-ldap_mailAttribute" class="error"></span>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" name="ldap.tls"@if(settings.ldap.flatMap(_.tls).getOrElse(false)){ checked}/> Enable TLS
</label>
<div class="form-group">
<label class="control-label col-md-3">Enable TLS</label>
<div class="col-md-9">
<input type="checkbox" name="ldap.tls"@if(settings.ldap.flatMap(_.tls).getOrElse(false)){ checked}/>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" name="ldap.ssl"@if(settings.ldap.flatMap(_.ssl).getOrElse(false)){ checked}/> Enable SSL
</label>
<div class="form-group">
<label class="control-label col-md-3">Enable SSL</label>
<div class="col-md-9">
<input type="checkbox" name="ldap.ssl"@if(settings.ldap.flatMap(_.ssl).getOrElse(false)){ checked}/>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapBindDN">Keystore</label>
<div class="controls">
<input type="text" id="ldapKeystore" name="ldap.keystore" value="@settings.ldap.map(_.keystore)"/>
<div class="form-group">
<label class="control-label col-md-3" for="ldapBindDN">Keystore</label>
<div class="col-md-9">
<input type="text" id="ldapKeystore" name="ldap.keystore" class="form-control" value="@settings.ldap.map(_.keystore)"/>
<span id="error-ldap_keystore" class="error"></span>
</div>
</div>
@@ -242,54 +238,53 @@
SMTP
</label>
</fieldset>
<div class="form-horizontal useSMTP">
<div class="control-group">
<label class="control-label" for="smtpHost">SMTP Host</label>
<div class="controls">
<input type="text" id="smtpHost" name="smtp.host" value="@settings.smtp.map(_.host)"/>
<div class="useSMTP">
<div class="form-group">
<label class="control-label col-md-3" for="smtpHost">SMTP Host</label>
<div class="col-md-9">
<input type="text" id="smtpHost" name="smtp.host" class="form-control" value="@settings.smtp.map(_.host)"/>
<span id="error-smtp_host" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="smtpPort">SMTP Port</label>
<div class="controls">
<input type="text" id="smtpPort" name="smtp.port" class="input-mini" value="@settings.smtp.map(_.port)"/>
<div class="form-group">
<label class="control-label col-md-3" for="smtpPort">SMTP Port</label>
<div class="col-md-9">
<input type="text" id="smtpPort" name="smtp.port" class="form-control input-mini" value="@settings.smtp.map(_.port)"/>
<span id="error-smtp_port" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="smtpUser">SMTP User</label>
<div class="controls">
<input type="text" id="smtpUser" name="smtp.user" value="@settings.smtp.map(_.user)"/>
<div class="form-group">
<label class="control-label col-md-3" for="smtpUser">SMTP User</label>
<div class="col-md-9">
<input type="text" id="smtpUser" name="smtp.user" class="form-control" value="@settings.smtp.map(_.user)"/>
</div>
</div>
<div class="control-group">
<label class="control-label" for="smtpPassword">SMTP Password</label>
<div class="controls">
<input type="password" id="smtpPassword" name="smtp.password" value="@settings.smtp.map(_.password)"/>
<div class="form-group">
<label class="control-label col-md-3" for="smtpPassword">SMTP Password</label>
<div class="col-md-9">
<input type="password" id="smtpPassword" name="smtp.password" class="form-control" value="@settings.smtp.map(_.password)"/>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" name="smtp.ssl"@if(settings.smtp.flatMap(_.ssl).getOrElse(false)){ checked}/> Enable SSL
</label>
<div class="form-group">
<label class="control-label col-md-3" for="smtpPassword">Enable SSL</label>
<div class="col-md-9">
<input type="checkbox" name="smtp.ssl"@if(settings.smtp.flatMap(_.ssl).getOrElse(false)){ checked}/>
</div>
</div>
<div class="control-group">
<label class="control-label" for="fromAddress">FROM Address</label>
<div class="controls">
<input type="text" id="fromAddress" name="smtp.fromAddress" value="@settings.smtp.map(_.fromAddress)"/>
<div class="form-group">
<label class="control-label col-md-3" for="fromAddress">FROM Address</label>
<div class="col-md-9">
<input type="text" id="fromAddress" name="smtp.fromAddress" class="form-control" value="@settings.smtp.map(_.fromAddress)"/>
</div>
</div>
<div class="control-group">
<label class="control-label" for="fromName">FROM Name</label>
<div class="controls">
<input type="text" id="fromName" name="smtp.fromName" value="@settings.smtp.map(_.fromName)"/>
<div class="form-group">
<label class="control-label col-md-3" for="fromName">FROM Name</label>
<div class="col-md-9">
<input type="text" id="fromName" name="smtp.fromName" class="form-control" value="@settings.smtp.map(_.fromName)"/>
</div>
</div>
<p class="muted">
Enable notification not only SMTP configuration if you want to send nitification email.
Enable notification not only SMTP configuration if you want to send notification email.
</p>
</div>
</div>
@@ -322,4 +317,4 @@ $(function(){
$('.ldap input').prop('disabled', !$(this).prop('checked'));
}).change();
});
</script>
</script>

View File

@@ -4,14 +4,14 @@
@html.main(if(account.isEmpty) "New Group" else "Update Group"){
@admin.html.menu("users"){
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_newgroup} else {@path/admin/users/@account.get.userName/_editgroup}" validate="true">
<div class="row-fluid">
<div class="span5">
<fieldset>
<div class="row">
<div class="col-md-5">
<fieldset class="form-group">
<label for="groupName" class="strong">Group name</label>
<div>
<span id="error-groupName" class="error"></span>
</div>
<input type="text" name="groupName" id="groupName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
<input type="text" name="groupName" id="groupName" class="form-control" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
@if(account.isDefined){
<label for="removed">
<input type="checkbox" name="removed" id="removed" value="true" @if(account.get.isRemoved){checked}/>
@@ -19,25 +19,25 @@
</label>
}
</fieldset>
<fieldset>
<fieldset class="form-group">
<label class="strong">URL (Optional)</label>
<div>
<span id="error-url" class="error"></span>
</div>
<input type="text" name="url" id="url" value="@account.map(_.url)"/>
<input type="text" name="url" id="url" class="form-control" value="@account.map(_.url)"/>
</fieldset>
<fieldset>
<fieldset class="form-group">
<label for="avatar" class="strong">Image (Optional)</label>
@helper.html.uploadavatar(account)
</fieldset>
</div>
<div class="span7">
<fieldset>
<div class="col-md-7">
<fieldset class="form-group">
<label class="strong">Members</label>
<ul id="member-list" class="collaborator">
</ul>
@helper.html.account("memberName", 200)
<input type="button" class="btn" value="Add" id="addMember"/>
<input type="button" class="btn btn-default" value="Add" id="addMember"/>
<input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.isManager).mkString(",")"/>
<div>
<span class="error" id="error-members"></span>
@@ -47,7 +47,7 @@
</div>
<fieldset class="margin">
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create Group} else {Update Group}"/>
<a href="@path/admin/users" class="btn">Cancel</a>
<a href="@path/admin/users" class="btn btn-default">Cancel</a>
</fieldset>
</form>
}

View File

@@ -4,8 +4,8 @@
@html.main("Manage Users"){
@admin.html.menu("users"){
<div class="pull-right" style="margin-bottom: 4px;">
<a href="@path/admin/users/_newuser" class="btn">New User</a>
<a href="@path/admin/users/_newgroup" class="btn">New Group</a>
<a href="@path/admin/users/_newuser" class="btn btn-default">New User</a>
<a href="@path/admin/users/_newgroup" class="btn btn-default">New Group</a>
</div>
<label for="includeRemoved">
<input type="checkbox" id="includeRemoved" name="includeRemoved" @if(includeRemoved){checked}/>

View File

@@ -3,14 +3,14 @@
@html.main(if(account.isEmpty) "New User" else "Update User"){
@admin.html.menu("users"){
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_newuser} else {@path/admin/users/@account.get.userName/_edituser}" validate="true">
<div class="row-fluid">
<div class="span6">
<fieldset>
<div class="row">
<div class="col-md-6">
<fieldset class="form-group">
<label for="userName" class="strong">Username:</label>
<div>
<span id="error-userName" class="error"></span>
</div>
<input type="text" name="userName" id="userName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
<input type="text" name="userName" id="userName" class="form-control" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
@if(account.isDefined){
<label for="removed">
<input type="checkbox" name="removed" id="removed" value="true" @if(account.get.isRemoved){checked}/>
@@ -22,7 +22,7 @@
}
</fieldset>
@if(account.map(_.password.nonEmpty).getOrElse(true)){
<fieldset>
<fieldset class="form-group">
<label for="password" class="strong">
Password
@if(account.isDefined){
@@ -33,24 +33,24 @@
<div>
<span id="error-password" class="error"></span>
</div>
<input type="password" name="password" id="password" value="" autocomplete="off"/>
<input type="password" name="password" id="password" class="form-control" value="" autocomplete="off"/>
</fieldset>
}
<fieldset>
<fieldset class="form-group">
<label for="fullName" class="strong">Full Name:</label>
<div>
<span id="error-fullName" class="error"></span>
</div>
<input type="text" name="fullName" id="fullName" value="@account.map(_.fullName)"/>
<input type="text" name="fullName" id="fullName" class="form-control" value="@account.map(_.fullName)"/>
</fieldset>
<fieldset>
<fieldset class="form-group">
<label for="mailAddress" class="strong">Mail Address:</label>
<div>
<span id="error-mailAddress" class="error"></span>
</div>
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
<input type="text" name="mailAddress" id="mailAddress" class="form-control" value="@account.map(_.mailAddress)"/>
</fieldset>
<fieldset>
<fieldset class="form-group">
<label class="strong">User Type:</label>
<label class="radio" for="userType_Normal">
<input type="radio" name="isAdmin" id="userType_Normal" value="false"@if(account.isEmpty || !account.get.isAdmin){ checked}/> Normal
@@ -59,16 +59,16 @@
<input type="radio" name="isAdmin" id="userType_Admin" value="true"@if(account.isDefined && account.get.isAdmin){ checked}/> Administrator
</label>
</fieldset>
<fieldset>
<fieldset class="form-group">
<label class="strong">URL (Optional):</label>
<div>
<span id="error-url" class="error"></span>
</div>
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
<input type="text" name="url" id="url" class="form-control" value="@account.map(_.url)"/>
</fieldset>
</div>
<div class="span6">
<fieldset>
<div class="col-md-6">
<fieldset class="form-group">
<label for="avatar" class="strong">Image (Optional)</label>
@helper.html.uploadavatar(account)
</fieldset>
@@ -76,7 +76,7 @@
</div>
<fieldset class="margin">
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create User} else {Update User}"/>
<a href="@path/admin/users" class="btn">Cancel</a>
<a href="@path/admin/users" class="btn btn-default">Cancel</a>
</fieldset>
</form>
}

View File

@@ -10,53 +10,63 @@
@import gitbucket.core.service.IssuesService
@import gitbucket.core.service.IssuesService.IssueInfo
<table class="table table-bordered table-hover table-issues">
<tr>
<th style="background-color: #eee;">
@dashboard.html.header(openCount, closedCount, condition, groups)
</th>
</tr>
@issues.map { case IssueInfo(issue, labels, milestone, commentCount, commitStatus) =>
<tr>
<td style="padding-top: 15px; padding-bottom: 15px;">
@if(issue.isPullRequest){
<i class="octicon octicon-git-pull-request @(if(issue.closed) "closed" else "open")"></i>
} else {
<i class="octicon octicon-issue-@(if(issue.closed) "closed" else "opened")"></i>
}
<a href="@path/@issue.userName/@issue.repositoryName">@issue.userName/@issue.repositoryName</a>&nbsp;&#xFF65;
@if(issue.isPullRequest){
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a>
}
@gitbucket.core.issues.html.commitstatus(issue, commitStatus)
@labels.map { label =>
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
}
<span class="pull-right muted">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20, tooltip = true)
}
@if(commentCount > 0){
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">
<i class="octicon octicon-comment active"></i> @commentCount
</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count" style="color: silver;">
<i class="octicon octicon-comment"></i> @commentCount
</a>
}
</span>
<div class="small muted" style="margin-left: 20px; margin-top: 5px;">
#@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
@milestone.map { milestone =>
<span style="margin: 20px;"><a href="@condition.copy(milestone = Some(Some(milestone))).toURL" class="username"><i class="octicon octicon-milestone"></i>
@milestone</a></span>
}
</div>
</td>
</tr>
}
<thead>
<tr>
<th style="background-color: #eee;">
@dashboard.html.header(openCount, closedCount, condition, groups)
</th>
</tr>
</thead>
<tbody>
@issues.map { case IssueInfo(issue, labels, milestone, commentCount, commitStatus) =>
<tr>
<td style="padding-top: 12px; padding-bottom: 12px;">
@if(issue.isPullRequest){
<i class="octicon octicon-git-pull-request @(if(issue.closed) "closed" else "open")"></i>
} else {
<i class="octicon octicon-issue-@(if(issue.closed) "closed" else "opened")"></i>
}
<a href="@path/@issue.userName/@issue.repositoryName">@issue.userName/@issue.repositoryName</a>&nbsp;&#xFF65;
@if(issue.isPullRequest){
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a>
}
@gitbucket.core.issues.html.commitstatus(issue, commitStatus)
@labels.map { label =>
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
}
<span class="pull-right muted">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20, tooltip = true)
}
@if(commentCount > 0){
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">
<i class="octicon octicon-comment active"></i> @commentCount
</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count" style="color: silver;">
<i class="octicon octicon-comment"></i> @commentCount
</a>
}
</span>
<div class="small muted" style="margin-left: 20px; margin-top: 2px;">
#@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
@milestone.map { milestone =>
<span style="margin: 20px;"><a href="@condition.copy(milestone = Some(Some(milestone))).toURL" class="username"><i class="octicon octicon-milestone"></i> @milestone</a></span>
}
</div>
</td>
</tr>
}
@if(issues.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
No results matched your search.
</td>
</tr>
}
</tbody>
</table>
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), IssuesService.IssueLimit, 10, condition.toURL)

View File

@@ -3,20 +3,18 @@
condition: gitbucket.core.service.IssuesService.IssueSearchCondition)(implicit context: gitbucket.core.controller.Context)
@import context._
@import gitbucket.core.view.helpers._
<ul class="nav nav-pills-group pull-left fill-width">
<li class="@if(filter == "created_by"){active} first">
<ul class="nav nav-pills pull-left" style="line-height: 14px;">
<li class="@if(filter == "created_by"){active}">
<a href="@path/dashboard/@active/created_by@condition.copy(author = None, assigned = None).toURL">Created</a>
</li>
<li class="@if(filter == "assigned"){active}">
<a href="@path/dashboard/@active/assigned@condition.copy(author = None, assigned = None).toURL">Assigned</a>
</li>
<li class="@if(filter == "mentioned"){active} last">
<li class="@if(filter == "mentioned"){active}">
<a href="@path/dashboard/@active/mentioned@condition.copy(author = None, assigned = None).toURL">Mentioned</a>
</li>
<li class="pull-right">
<form method="GET" id="search-filter-form" action="@path/dashboard/@active" style="margin-bottom: 0px;">
<input type="text" id="search-filter-box" class="input-xlarge" name="q" style="height: 24px; width: 400px;"
value="is:@{if(active == "issues") "issue" else "pr"} @condition.toFilterString"/>
</form>
</li>
</ul>
<form method="GET" id="search-filter-form" action="@path/dashboard/@active" class="pull-right">
<input type="text" id="search-filter-box" class="form-control input-lg" name="q" style="width: 400px;"
value="is:@{if(active == "issues") "issue" else "pr"} @condition.toFilterString"/>
</form>

View File

@@ -4,17 +4,14 @@
<div class="dashboard-nav">
<div class="container">
<a href="@path/" @if(active == ""){ class="active"}>
<i class="octicon octicon-rss"></i>
News Feed
<i class="octicon octicon-rss"></i> News Feed
</a>
@if(loginAccount.isDefined){
<a href="@path/dashboard/pulls" @if(active == "pulls" ){ class="active"}>
<i class="octicon octicon-git-pull-request"></i>
Pull Requests
<i class="octicon octicon-git-pull-request"></i> Pull Requests
</a>
<a href="@path/dashboard/issues" @if(active == "issues"){ class="active"}>
<i class="octicon octicon-issue-opened"></i>
Issues
<i class="octicon octicon-issue-opened"></i> Issues
</a>
}
</div>
@@ -27,8 +24,6 @@ div.dashboard-nav {
margin-bottom: 20px;
}
div.dashboard-nav a {
line-height: 10px;
margin-left: 20px;
@@ -38,7 +33,6 @@ div.dashboard-nav a {
color: #888;
}
div.dashboard-nav .octicon{
width: 16px;
height: 16px;

View File

@@ -1,6 +1,8 @@
@(id: String, width: Int)(implicit context: gitbucket.core.controller.Context)
@import context._
<input type="text" name="@id" id="@id" style="width: @{width}px; margin-bottom: 0px;"/>
<span style="margin-right: 0px;">
<input type="text" name="@id" id="@id" class="form-control" autocomplete="off" style="width: @{width}px; margin-bottom: 0px; display: inline; vertical-align: middle;"/>
</span>
<script>
$(function(){
$('#@id').typeahead({

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();
}
});
@@ -27,7 +29,10 @@ $(function(){
}
// Adjust clickable area width
$('#@textareaId').next('div.clickable').css('width', ($('#@textareaId').width() + 8) + 'px');
$('#@textareaId').next('div.clickable').css({
'width': ($('#@textareaId').width() + 18) + 'px',
'font-size': '13px'
});
});
</script>
}

View File

@@ -7,11 +7,10 @@
@import gitbucket.core.view.helpers._
@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 = false
prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree"
) {
<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>
<li><input id="branch-control-input" type="text" class="form-control input-sm" placeholder="Find or create branch ..."/></li>
@body
@if(hasWritePermission) {
<li id="create-branch" style="display: none;">

View File

@@ -2,5 +2,5 @@
@if(condition){
<i class="octicon octicon-check"></i>
} else {
<i class="icon-white"></i>
<i class="octicon"></i>
}

View File

@@ -6,18 +6,19 @@
@import gitbucket.core._
@import gitbucket.core.view.helpers._
<div class="@if(comment.fileName.isDefined && (!latestCommitId.isDefined || latestCommitId.get == comment.commitId)){inline-comment}"
id="discussion_r@comment.commentId"
@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">
<div class="panel- panel-default commit-comment-box commit-comment-@comment.commentId">
<div class="panel-heading">
@user(comment.commentedUserName, styleClass="username strong")
<span class="muted">
commented
@if(comment.pullRequest){
@if(comment.issueId.isDefined){
on this Pull Request
}else{
} else {
@if(comment.fileName.isDefined){
on @comment.fileName.get
}
@@ -32,8 +33,16 @@
}
</span>
</div>
<div class="box-content-bottom commit-commentContent-@comment.commentId markdown-body">
@markdown(comment.content, repository, false, true, true, hasWritePermission)
<div class="panel-body issue-content commit-commentContent-@comment.commentId markdown-body">
@markdown(
markdown = comment.content,
repository = repository,
enableWikiLink = false,
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = false,
hasWritePermission = hasWritePermission
)
</div>
</div>
</div>

View File

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

View File

@@ -1,11 +1,13 @@
@(name: String, value: Option[java.util.Date])
@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="octicon octicon-calendar"></i></span>
<div id="@name" class="input-group date" data-date="@value.map(helpers.date)" style="width: 180px;">
<input class="form-control" name="@name" type="text" value="@value.map(helpers.date)"/>
<span class="input-group-addon"><i class="octicon octicon-calendar"></i></span>
</div>
<script>
$(function(){
$('#@name').datepicker();
$('#@name').datetimepicker({
format: 'YYYY-MM-DD'
});
});
</script>

View File

@@ -45,13 +45,13 @@
<a name="diff-@i"></a>
<table class="table table-bordered diff-outside" commitId="@newCommitId" fileName="@diff.newPath" data-diff-id="@i">
<tr>
<th style="font-weight: normal; line-height: 27px;" class="box-header">
<th style="font-weight: normal;" class="box-header">
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
@if(newCommitId.isDefined){
<div class="pull-right align-right">
<label class="checkbox" style="display: inline-block;"><input type="checkbox" class="ignore-whitespace" value="1"/>Ignore Space</label>
<label class="checkbox" style="display: inline-block;"><input type="checkbox" class="toggle-notes" checked><span>Show notes</span></label>
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-small" title="View the whole file at version @newCommitId.get.substring(0, 10)" data-toggle="tooltip">View</a>
<label class="checkbox no-margin" style="display: inline-block;"><input type="checkbox" class="ignore-whitespace" value="1"/>Ignore Space</label>
<label class="checkbox no-margin" style="display: inline-block;"><input type="checkbox" class="toggle-notes" checked><span>Show notes</span></label>
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-default btn-sm" title="View the whole file at version @newCommitId.get.substring(0, 10)" data-toggle="tooltip">View</a>
</div>
}
<span class="diffstat">
@@ -61,9 +61,9 @@
@if(diff.changeType == ChangeType.ADD || diff.changeType == ChangeType.MODIFY){
@if(newCommitId.isDefined){
<div class="pull-right align-right">
<label class="checkbox" style="display: inline-block;"><input type="checkbox" class="ignore-whitespace" value="1"/>Ignore Space</label>
<label class="checkbox" style="display: inline-block;"><input type="checkbox" class="toggle-notes" checked><span>Show notes</span></label>
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-small" title="View the whole file at version @newCommitId.get.substring(0, 10)" data-toggle="tooltip">View</a>
<label class="checkbox no-margin" style="display: inline-block;"><input type="checkbox" class="ignore-whitespace" value="1"/>Ignore Space</label>
<label class="checkbox no-margin" style="display: inline-block;"><input type="checkbox" class="toggle-notes" checked><span>Show notes</span></label>
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-default btn-sm" title="View the whole file at version @newCommitId.get.substring(0, 10)" data-toggle="tooltip">View</a>
</div>
}
<span class="diffstat">
@@ -78,8 +78,8 @@
@if(diff.changeType == ChangeType.DELETE){
@if(oldCommitId.isDefined){
<div class="pull-right align-right">
<label class="checkbox" style="display: inline-block;"><input type="checkbox" class="toggle-notes" checked><span>Show notes</span></label>
<a href="@url(repository)/blob/@oldCommitId.get/@diff.oldPath" class="btn btn-small" title="View the whole file at version @oldCommitId.get.substring(0, 10)" data-toggle="tooltip">View</a>
<label class="checkbox no-margin" style="display: inline-block;"><input type="checkbox" class="toggle-notes" checked><span>Show notes</span></label>
<a href="@url(repository)/blob/@oldCommitId.get/@diff.oldPath" class="btn btn-default btn-sm" title="View the whole file at version @oldCommitId.get.substring(0, 10)" data-toggle="tooltip">View</a>
</div>
}
<span class="diffstat">
@@ -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)/raw/@oldCommitId.get/@diff.oldPath" 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)/raw/@newCommitId.get/@diff.newPath" 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>
@@ -212,7 +220,6 @@ $(function(){
dataType : 'html'
},
function(responseContent) {
$this.hide();
var tmp;
if (!isNaN(oldLineNumber) && oldLineNumber) {
if (!isNaN(newLineNumber) && newLineNumber) {

View File

@@ -1,13 +1,12 @@
@(value : String = "",
prefix: String = "",
mini : Boolean = true,
style : String = "",
right : Boolean = false,
flat : Boolean = false)(body: Html)
<div class="btn-group" @if(style.nonEmpty){style="@style"}>
<button
@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">
class="dropdown-toggle @if(!flat){btn btn-default} else {flat} btn-sm" data-toggle="dropdown">
@if(value.isEmpty){
<i class="octicon octicon-gear"></i>
} else {

View File

@@ -2,17 +2,18 @@
groupAndPerm: Map[String, Boolean])(implicit context: gitbucket.core.controller.Context)
@import context._
@import gitbucket.core.view.helpers._
<h2 class="facebox-header">Where should we fork this repository?</h2>
<form action="@url(repository)/fork" id="fork" method="post">
<div class="owner-select-grid">
<div class="owner-select-target js-fork-owner-select-target enabled">@avatar(loginAccount.get.userName, 100)<span class="owner css-truncate" title="@@@loginAccount.get.userName">@@@loginAccount.get.userName</span></div>
<h2 class="facebox-header">Where should we fork this repository?</h2>
<form action="@url(repository)/fork" id="fork" method="post">
<div class="owner-select-grid">
<div class="owner-select-target js-fork-owner-select-target enabled">@avatar(loginAccount.get.userName, 100)<span class="owner css-truncate" title="@@@loginAccount.get.userName">@@@loginAccount.get.userName</span></div>
@for((groupName, isManager) <- groupAndPerm) {
@if(isManager) {
<div class="owner-select-target js-fork-owner-select-target enabled">@avatar(groupName, 100)<span class="owner css-truncate" title="@@@groupName">@@@groupName</span></div>
<div class="owner-select-target js-fork-owner-select-target enabled">@avatar(groupName, 100)<span class="owner css-truncate" title="@@@groupName">@@@groupName</span></div>
} else {
<div title="You don't have permission to fork here." class="owner-select-target js-fork-owner-select-target disabled">@avatar(groupName, 100)<span class="owner css-truncate" title="@@@groupName">@@@groupName</span></div>
<div title="You don't have permission to fork here." class="owner-select-target js-fork-owner-select-target disabled">@avatar(groupName, 100)<span class="owner css-truncate" title="@@@groupName">@@@groupName</span></div>
}
}
</div>
<input id="account" name="account" type="hidden" />
</div>
<input id="account" name="account" type="hidden" />
</form>

View File

@@ -2,26 +2,29 @@
content: String,
enableWikiLink: Boolean,
enableRefsLink: Boolean,
enableLineBreaks: Boolean,
enableTaskList: Boolean,
hasWritePermission: Boolean,
style: String = "",
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._
@import gitbucket.core.view.helpers._
<div class="tabbable">
<ul class="nav nav-tabs" style="height: 37px;">
<ul class="nav nav-tabs fill-width pull-left" style="@*margin-top: 12px; *@margin-bottom: 10px;">
<li class="active"><a href="#tab@uid" data-toggle="tab">Write</a></li>
<li><a href="#tab@(uid+1)" data-toggle="tab" id="preview@uid">Preview</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="tab@uid">
<div class="tab-pane active" style="margin-top: 4px;" id="tab@uid">
<span id="error-content" class="error"></span>
@textarea = {
<textarea id="content@uid" name="content" placeholder="@placeholder"
<textarea id="content@uid" class="form-control" name="content" placeholder="@placeholder"
@if(tabIndex > -2){ tabindex="@tabIndex"}
@if(style.nonEmpty){ style="@style"}
@if(styleClass.nonEmpty){ class="@styleClass" }>@content</textarea>
}
@@ -48,10 +51,11 @@ $(function(){
$('#preview@uid').click(function(){
$('#preview-area@uid').html('<img src="@assets/common/images/indicator.gif"> Previewing...');
$.post('@url(repository)/_preview', {
content : $('#content@uid').val(),
enableWikiLink : @enableWikiLink,
enableRefsLink : @enableRefsLink,
enableTaskList : @enableTaskList
content : $('#content@uid').val(),
enableWikiLink : @enableWikiLink,
enableRefsLink : @enableRefsLink,
enableLineBreaks : @enableLineBreaks,
enableTaskList : @enableTaskList
}, function(data){
$('#preview-area@uid').html(data);
prettyPrint();

View File

@@ -6,15 +6,15 @@
@main("GitBucket"){
@dashboard.html.tab()
<div class="container">
<div class="row-fluid">
<div class="span8">
<div class="row">
<div class="col-md-8">
<div class="pull-right">
<a href="@path/activities.atom"><img src="@assets/common/images/feed.png" alt="activities"></a>
</div>
@helper.html.activities(activities)
</div>
<div class="span4">
<div class="col-md-4">
@settings.information.map { information =>
<div class="alert alert-info" style="background-color: white; color: #555; border-color: #4183c4; font-size: small; line-height: 120%;">
<button type="button" class="close" data-dismiss="alert">&times;</button>
@@ -24,55 +24,53 @@
@if(loginAccount.isEmpty){
@signinform(settings)
} else {
<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){
<div class="box-content-bottom">
No repositories
<div class="panel panel-default">
<div class="panel-heading strong">
<div class="pull-right">
<a href="@path/new" class="btn btn-success btn-sm">New repository</a>
</div>
} else {
<div class="box-content-bottom" style="padding: 0px;">
@defining(3){ max =>
Your repositories <span class="badge">@userRepositories.size</span>
</div>
<ul class="list-group list-group-flush">
@if(userRepositories.isEmpty){
<li class="list-group-item">No repositories</li>
} else {
@defining(20){ max =>
@userRepositories.zipWithIndex.map { case (repository, i) =>
<div class="box-content-row repo-link" style="@if(i > max - 1){display:none;}">
<li class="list-group-item 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>
</li>
}
@if(userRepositories.size > max){
<div class="box-content-row show-more">
<li class="list-group-item show-more">
<a href="javascript:void(0);" id="show-more-repos">Show more @{userRepositories.size - max} pages...</a>
</div>
</li>
}
}
</div>
}
}
</ul>
</div>
}
<div class="box-header">
<span class="strong">Recent updated repositories</span>
<div class="panel panel-default">
<div class="panel-heading strong">Recent updated repositories</div>
<ul class="list-group list-group-flush">
@if(recentRepositories.isEmpty){
<li class="list-group-item">No repositories</li>
} else {
@recentRepositories.map { repository =>
<li class="list-group-item repo-link">
@helper.html.repositoryicon(repository, false)
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
</li>
}
}
</ul>
</div>
@if(recentRepositories.isEmpty){
<div class="box-content-bottom">
No repositories
</div>
} else {
<div class="box-content-bottom" style="padding: 0px;">
@recentRepositories.map { repository =>
<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>
}
</div>
}
</div>
</div>
</div>
@@ -80,8 +78,8 @@
<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();
$(e.target).parents('ul.list-group').find('li.repo-link').show();
$(e.target).parents('li.show-more').remove();
});
});
</script>
</script>

View File

@@ -8,24 +8,26 @@
<hr/><br/>
<form method="POST" validate="true">
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
<div class="issue-comment-box">
<div class="box-content">
<div class="panel panel-default issue-comment-box">
<div class="panel-body">
@helper.html.preview(
repository = repository,
content = "",
enableWikiLink = false,
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = hasWritePermission,
style = "",
elastic = true
elastic = true,
tabIndex = 1
)
<div style="text-align: right;">
<div class="text-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"/>
<input type="submit" class="btn btn-lg btn-default" tabindex="3" formaction="@url(repository)/issue_comments/state" value="@{if(issue.closed) "Reopen" else "Close"}" id="action"/>
}
<input type="submit" class="btn btn-lg btn-success" tabindex="2" formaction="@url(repository)/issue_comments/new" value="Comment"/>
</div>
</div>
</div>

View File

@@ -8,8 +8,8 @@
@import gitbucket.core.model.CommitComment
@if(issue.isDefined){
<div class="issue-avatar-image">@avatar(issue.get.openedUserName, 48)</div>
<div class="issue-comment-box">
<div class="box-header">
<div class="panel panel-default issue-comment-box">
<div class="panel-heading">
@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)){
@@ -17,8 +17,16 @@
}
</span>
</div>
<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 class="panel-body issue-content markdown-body" id="issueContent">
@markdown(
markdown = issue.get.content getOrElse "No description provided.",
repository = repository,
enableWikiLink = false,
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = hasWritePermission
)
</div>
</div>
}
@@ -27,8 +35,8 @@
case comment: gitbucket.core.model.IssueComment => {
@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">
<div class="panel panel-default issue-comment-box" id="comment-@comment.commentId">
<div class="panel-heading">
@user(comment.commentedUserName, styleClass="username strong")
<span class="muted">
@if(comment.action == "comment"){
@@ -38,27 +46,45 @@
}
@helper.html.datetimeago(comment.registeredDate)
</span>
<span class="pull-right">
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer"
&& (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer"
&& (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
<span class="pull-right">
<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-bottom issue-content" id="commentContent-@comment.commentId">
</span>
}
@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>
<div class="markdown-body">@markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true, true, hasWritePermission)</div>
<span class="pull-right"><a href="@path/@repository.owner/@repository.name/commit/@id" class="monospace">@id.substring(0, 7)</a></span>
}
}
</div>
<div class="panel-body issue-content markdown-body" id="commentContent-@comment.commentId">
@if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){
@markdown(
markdown = comment.content.substring(0, comment.content.length - 41),
repository = repository,
enableWikiLink = false,
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = hasWritePermission
)
} 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>
@markdown(
markdown = comment.content,
repository = repository,
enableWikiLink = false,
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = hasWritePermission
)
}
}
</div>

View File

@@ -7,33 +7,32 @@
@import gitbucket.core.view.helpers._
@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("issues", repository){
@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="span10">
<form action="@url(repository)/issues/new" method="POST" validate="true" class="form-group">
<div class="row">
<div class="col-md-10">
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
<div class="box issue-box">
<div class="box-content">
<div class="panel panel-default issue-box">
<div class="panel-body">
<span id="error-title" class="error"></span>
<input type="text" name="title" value="" placeholder="Title" style="width: 690px;" autofocus/>
<input type="text" name="title" class="form-control input-lg" value="" placeholder="Title" style="width: 680px;" autofocus/>
@helper.html.preview(
repository = repository,
content = "",
enableWikiLink = false,
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = hasWritePermission,
style = "width: 690px; height: 200px; max-height: 250px;",
style = "width: 680px; height: 200px; max-height: 250px;",
elastic = true
)
<div class="align-right">
<input type="submit" class="btn btn-success" value="Submit new issue"/>
<input type="submit" class="btn btn-lg btn-success" value="Submit new issue"/>
</div>
</div>
</div>
</div>
<div class="span2">
<div class="col-md-2">
@issueinfo(None, Nil, Nil, collaborators, milestones.map(x => (x, 0, 0)), labels, hasWritePermission, repository)
</div>
</div>

View File

@@ -2,11 +2,11 @@
@import context._
<span id="error-edit-content-@commentId" class="error"></span>
@helper.html.attached(owner, repository){
<textarea id="edit-content-@commentId">@content</textarea>
<textarea id="edit-content-@commentId" class="form-control">@content</textarea>
}
<div>
<input type="button" id="cancel-comment-@commentId" class="btn btn-small btn-danger" value="Cancel"/>
<input type="button" id="update-comment-@commentId" class="btn btn-small pull-right" value="Update comment"/>
<input type="button" id="cancel-comment-@commentId" class="btn btn-lg btn-danger" value="Cancel"/>
<input type="button" id="update-comment-@commentId" class="btn btn-lg btn-default pull-right" value="Update comment"/>
</div>
<script>
$(function(){

View File

@@ -1,11 +1,11 @@
@(content: Option[String], issueId: Int, owner: String, repository: String)(implicit context: gitbucket.core.controller.Context)
@import context._
@helper.html.attached(owner, repository){
<textarea id="edit-content">@content.getOrElse("")</textarea>
<textarea id="edit-content" class="form-control">@content.getOrElse("")</textarea>
}
<div>
<input type="button" id="cancel-issue" class="btn btn-small btn-danger" value="Cancel"/>
<input type="button" id="update-issue" class="btn btn-small pull-right" value="Update comment"/>
<input type="button" id="cancel-issue" class="btn btn-lg btn-danger" value="Cancel"/>
<input type="button" id="update-issue" class="btn btn-lg btn-default pull-right" value="Update comment"/>
</div>
<script>
$(function(){

View File

@@ -13,12 +13,12 @@
<div>
<div class="show-title pull-right">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a class="btn" href="#" id="edit">Edit</a>
<a class="btn btn-default" href="#" id="edit">Edit</a>
}
<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>
<a class="btn btn-default" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a>
</div>
<h1>
<span class="show-title">
@@ -27,31 +27,31 @@
</span>
<span class="edit-title" style="display: none;">
<span id="error-edit-title" class="error"></span>
<input type="text" style="width: 700px;" id="edit-title" value="@issue.title"/>
<input type="text" class="form-control" style="width: 700px;" id="edit-title" value="@issue.title"/>
</span>
</h1>
</div>
@if(issue.closed) {
<span class="label label-important issue-status">Closed</span>
} else {
<span class="label label-success issue-status">Open</span>
}
<span class="muted">
@user(issue.openedUserName, styleClass="username strong") opened this issue @helper.html.datetimeago(issue.registeredDate) - @defining(
comments.count( _.action.contains("comment") )
){ count =>
@count @plural(count, "comment")
<div style="margin-bottom: 15px">
@if(issue.closed) {
<span class="label label-important issue-status">Closed</span>
} else {
<span class="label label-success issue-status">Open</span>
}
</span>
<br/><br/>
<span class="muted">
@user(issue.openedUserName, styleClass="username strong") opened this issue @helper.html.datetimeago(issue.registeredDate) - @defining(
comments.count( _.action.contains("comment") )
){ count =>
@count @plural(count, "comment")
}
</span>
</div>
<hr>
<br/>
<div class="row-fluid">
<div class="span10">
<div class="row" style="margin-top: 15px;">
<div class="col-md-10">
@commentlist(Some(issue), comments, hasWritePermission, repository)
@commentform(issue, true, hasWritePermission, repository)
</div>
<div class="span2">
<div class="col-md-2">
@issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
</div>
</div>

View File

@@ -93,7 +93,7 @@
@collaborators.map { collaborator =>
<li>
<a href="javascript:void(0);" class="assign" data-name="@collaborator">
@helper.html.checkicon(issue.exists(_.assignedUserName == collaborator))@avatar(collaborator, 20) @collaborator
@helper.html.checkicon(issue.exists(_.assignedUserName.exists(_ == collaborator)))@avatar(collaborator, 20) @collaborator
</a>
</li>
}
@@ -116,11 +116,13 @@
<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>
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
@participants.map { participant =>
@avatarLink(participant, 20, tooltip = true)
}
}
}
<script>
$(function(){
@@ -162,7 +164,7 @@ $(function(){
switchLabel($(this));
var labelNames = Array();
$('a.toggle-label').each(function(i, e){
if($(e).children('i').hasClass('icon-ok') == true){
if($(e).children('i').hasClass('octicon-check') == true){
labelNames.push($(e).text().trim());
}
});
@@ -193,17 +195,17 @@ $(function(){
function switchLabel($this){
var i = $this.children('i');
if(i.hasClass('icon-ok')){
i.removeClass().addClass('icon-white');
if(i.hasClass('octicon-check')){
i.removeClass('octicon-check');
return 'delete';
} else {
i.removeClass().addClass('icon-ok');
i.addClass('octicon-check');
return 'new';
}
}
function displayMilestone(title, milestoneId, progress){
$('a.milestone i.icon-ok').attr('class', 'icon-white');
$('a.milestone i.octicon-check').removeClass('octicon-check');
if(milestoneId == ''){
$('#label-milestone').html($('<span class="muted small">').text('No milestone'));
$('#milestone-progress-area').empty();
@@ -212,19 +214,19 @@ $(function(){
if(progress){
$('#milestone-progress-area').html(progress);
}
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
$('a.milestone[data-id=' + milestoneId + '] i').addClass('octicon-check');
}
}
function displayAssignee($this, userName){
$('a.assign i.icon-ok').attr('class', 'icon-white');
$('a.assign i.octicon-check').removeClass('octicon-check');
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');
$('a.assign[data-name=' + jqSelectorEscape(userName) + '] i').addClass('octicon-check');
}
}
});

View File

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

View File

@@ -6,8 +6,8 @@
@import gitbucket.core.view.helpers._
<tr id="label-row-@label.labelId">
<td style="padding-top: 15px; padding-bottom: 15px;">
<div class="milestone row-fluid" id="label-@label.labelId">
<div class="span8">
<div class="milestone row" id="label-@label.labelId">
<div class="col-md-8">
<div style="margin-top: 6px">
<a href="@url(repository)/issues?labels=@urlEncode(label.labelName)" id="label-row-content-@label.labelId">
<span style="background-color: #@label.color; color: #@label.fontColor; padding: 8px; font-size: 120%; border-radius: 4px;">
@@ -17,13 +17,13 @@
</a>
</div>
</div>
<div class="@if(hasWritePermission){span2} else {span4}">
<div class="@if(hasWritePermission){col-md-2} else {col-md-4}">
<div class="pull-right">
<span class="muted">@counts.get(label.labelName).getOrElse(0) open issues</span>
</div>
</div>
@if(hasWritePermission){
<div class="span2">
<div class="col-md-2">
<div class="pull-right">
<a href="javascript:void(0);" onclick="editLabel(@label.labelId)">Edit</a>
&nbsp;&nbsp;

View File

@@ -12,24 +12,28 @@
<tr><td></td></tr>
</table>
<table class="table table-bordered table-hover table-issues">
<tr id="label-row-header">
<th style="background-color: #eee;">
<span class="small">@labels.size labels</span>
</th>
</tr>
@labels.map { label =>
@gitbucket.core.issues.labels.html.label(label, counts, repository, hasWritePermission)
}
@if(labels.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
No labels to show.
@if(hasWritePermission){
<a href="@url(repository)/issues/labels/new">Create a new label.</a>
}
</td>
<thead>
<tr id="label-row-header">
<th style="background-color: #eee;">
<span class="small">@labels.size labels</span>
</th>
</tr>
}
</thead>
<tbody>
@labels.map { label =>
@gitbucket.core.issues.labels.html.label(label, counts, repository, hasWritePermission)
}
@if(labels.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
No labels to show.
@if(hasWritePermission){
<a href="@url(repository)/issues/labels/new">Create a new label.</a>
}
</td>
</tr>
}
</tbody>
</table>
}
}

View File

@@ -12,9 +12,8 @@
@import context._
@import gitbucket.core.view.helpers._
@import gitbucket.core.service.IssuesService.IssueInfo
<br>
@if(condition.nonEmpty){
<div>
<div style="width: 100%; display: inline-block;">
<a href="@gitbucket.core.service.IssuesService.IssueSearchCondition().toURL" class="header-link">
<i class="octicon octicon-x" ></i>
<span class="strong">Clear current search query, filters, and sorts</span>
@@ -22,196 +21,199 @@
</div>
}
<table class="table table-bordered table-hover table-issues">
<tr>
<th style="background-color: #eee;">
<input type="checkbox"/>
<span class="small">
<a class="button-link@if(condition.state == "open"){ selected}" href="@condition.copy(state = "open").toURL">
<i class="octicon octicon-issue-opened @(if(condition.state == "open"){"active"})"></i>
@openCount Open
</a>&nbsp;&nbsp;
<a class="button-link@if(condition.state == "closed"){ selected}" href="@condition.copy(state = "closed").toURL">
<i class="octicon octicon-check @(if(condition.state == "closed"){"active"})"></i>
@closedCount Closed
</a>
</span>
<div class="pull-right" id="table-issues-control">
@helper.html.dropdown("Author", flat = true) {
@collaborators.map { collaborator =>
<li>
<a href="@condition.copy(author = (if(condition.author == Some(collaborator)) None else Some(collaborator))).toURL">
@helper.html.checkicon(condition.author == Some(collaborator))
@avatar(collaborator, 20) @collaborator
</a>
</li>
}
}
@helper.html.dropdown("Label", flat = true) {
@labels.map { label =>
<li>
<a href="@condition.copy(labels = (if(condition.labels.contains(label.labelName)) condition.labels - label.labelName else condition.labels + label.labelName)).toURL">
@helper.html.checkicon(condition.labels.contains(label.labelName))
<span style="background-color: #@label.color;" class="label-color">&nbsp;&nbsp;</span>
@label.labelName
</a>
</li>
}
}
@helper.html.dropdown("Milestone", flat = true) {
<li>
<a href="@condition.copy(milestone = Some(None)).toURL">
@helper.html.checkicon(condition.milestone == Some(None)) Issues with no milestone
</a>
</li>
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
<li>
<a href="@condition.copy(milestone = Some(Some(milestone.title))).toURL">
@helper.html.checkicon(condition.milestone == Some(Some(milestone.title))) @milestone.title
</a>
</li>
}
}
@helper.html.dropdown("Assignee", flat = true) {
@collaborators.map { collaborator =>
<li>
<a href="@condition.copy(assigned = Some(collaborator)).toURL">
@helper.html.checkicon(condition.assigned == Some(collaborator))
@avatar(collaborator, 20) @collaborator
</a>
</li>
}
}
@helper.html.dropdown("Sort", flat = true){
<li>
<a href="@condition.copy(sort="created", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
</a>
</li>
<li>
<a href="@condition.copy(sort="created", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
</a>
</li>
<li>
<a href="@condition.copy(sort="comments", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
</a>
</li>
<li>
<a href="@condition.copy(sort="comments", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
</a>
</li>
<li>
<a href="@condition.copy(sort="updated", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
</a>
</li>
<li>
<a href="@condition.copy(sort="updated", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
</a>
</li>
}
</div>
@if(hasWritePermission){
<div class="pull-right" id="table-issues-batchedit">
@helper.html.dropdown("Mark as", flat = true) {
<li><a href="javascript:void(0);" class="toggle-state" data-id="open">Open</a></li>
<li><a href="javascript:void(0);" class="toggle-state" data-id="close">Close</a></li>
<thead>
<tr>
<th style="background-color: #eee;">
<input type="checkbox"/>
<span class="small">
<a class="button-link@if(condition.state == "open"){ selected}" href="@condition.copy(state = "open").toURL">
<i class="octicon octicon-issue-opened @(if(condition.state == "open"){"active"})"></i>
@openCount Open
</a>&nbsp;&nbsp;
<a class="button-link@if(condition.state == "closed"){ selected}" href="@condition.copy(state = "closed").toURL">
<i class="octicon octicon-check @(if(condition.state == "closed"){"active"})"></i>
@closedCount Closed
</a>
</span>
<div class="pull-right" id="table-issues-control">
@helper.html.dropdown("Author", flat = true) {
@collaborators.map { collaborator =>
<li>
<a href="@condition.copy(author = (if(condition.author == Some(collaborator)) None else Some(collaborator))).toURL">
@helper.html.checkicon(condition.author == Some(collaborator))
@avatar(collaborator, 20) @collaborator
</a>
</li>
}
}
@helper.html.dropdown("Label", flat = true) {
@labels.map { label =>
<li>
<a href="javascript:void(0);" class="toggle-label" data-id="@label.labelId">
<i class="icon-white"></i>
<span class="label" style="background-color: #@label.color;">&nbsp;</span>
<a href="@condition.copy(labels = (if(condition.labels.contains(label.labelName)) condition.labels - label.labelName else condition.labels + label.labelName)).toURL">
@helper.html.checkicon(condition.labels.contains(label.labelName))
<span style="background-color: #@label.color;" class="label-color">&nbsp;&nbsp;</span>
@label.labelName
</a>
</li>
}
}
@helper.html.dropdown("Milestone", flat = true) {
<li><a href="javascript:void(0);" class="toggle-milestone" data-id="">No milestone</a></li>
<li>
<a href="@condition.copy(milestone = Some(None)).toURL">
@helper.html.checkicon(condition.milestone == Some(None)) Issues with no milestone
</a>
</li>
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
<li><a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId">@milestone.title</a></li>
<li>
<a href="@condition.copy(milestone = Some(Some(milestone.title))).toURL">
@helper.html.checkicon(condition.milestone == Some(Some(milestone.title))) @milestone.title
</a>
</li>
}
}
@helper.html.dropdown("Assignee", flat = true) {
<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>
<li>
<a href="@condition.copy(assigned = Some(collaborator)).toURL">
@helper.html.checkicon(condition.assigned == Some(collaborator))
@avatar(collaborator, 20) @collaborator
</a>
</li>
}
}
@helper.html.dropdown("Sort", flat = true){
<li>
<a href="@condition.copy(sort="created", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
</a>
</li>
<li>
<a href="@condition.copy(sort="created", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
</a>
</li>
<li>
<a href="@condition.copy(sort="comments", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
</a>
</li>
<li>
<a href="@condition.copy(sort="comments", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
</a>
</li>
<li>
<a href="@condition.copy(sort="updated", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
</a>
</li>
<li>
<a href="@condition.copy(sort="updated", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
</a>
</li>
}
</div>
}
</th>
</tr>
@if(issues.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
@if(target == "issues"){
No issues to show.
} else {
No pull requests to show.
}
@if(condition.labels.nonEmpty || condition.milestone.isDefined){
<a href="@condition.copy(labels = Set.empty, milestone = None).toURL">Clear active filters.</a>
} else {
@if(repository.isDefined){
@if(target == "issues"){
<a href="@url(repository.get)/issues/new">Create a new issue.</a>
} else {
<a href="@url(repository.get)/compare">Create a new pull request.</a>
}
}
}
</td>
</tr>
}
@issues.map { case IssueInfo(issue, labels, milestone, commentCount, commitStatus) =>
<tr>
<td style="padding-top: 15px; padding-bottom: 15px;">
@if(hasWritePermission){
<input type="checkbox" value="@issue.issueId"/>
<div class="pull-right" id="table-issues-batchedit">
@helper.html.dropdown("Mark as", flat = true) {
<li><a href="javascript:void(0);" class="toggle-state" data-id="open">Open</a></li>
<li><a href="javascript:void(0);" class="toggle-state" data-id="close">Close</a></li>
}
@helper.html.dropdown("Label", flat = true) {
@labels.map { label =>
<li>
<a href="javascript:void(0);" class="toggle-label" data-id="@label.labelId">
<i class="octicon"></i>
<span class="label" style="background-color: #@label.color;">&nbsp;</span>
@label.labelName
</a>
</li>
}
}
@helper.html.dropdown("Milestone", flat = true) {
<li><a href="javascript:void(0);" class="toggle-milestone" data-id="">No milestone</a></li>
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
<li><a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId">@milestone.title</a></li>
}
}
@helper.html.dropdown("Assignee", flat = true) {
<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="octicon"></i>@avatar(collaborator, 20) @collaborator</a></li>
}
}
</div>
}
<i class="octicon octicon-issue-@(if(issue.closed) "closed" else "opened")" style="margin-right: 3px;"></i>
@if(repository.isEmpty){
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a>&nbsp;&#xFF65;
}
@if(target == "issues"){
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
}
@commitstatus(issue, commitStatus)
@labels.map { label =>
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
}
<span class="pull-right small">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20, tooltip = true)
}
@if(commentCount > 0){
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">
<i class="octicon octicon-comment active"></i> @commentCount
</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count" style="color: silver;">
<i class="octicon octicon-comment"></i> @commentCount
</a>
}
</span>
<div class="small muted" style="margin-left: 40px; margin-top: 5px;">
#@issue.issueId opened @helper.html.datetimeago(issue.registeredDate) by @user(issue.openedUserName, styleClass="username")
@milestone.map { milestone =>
<span style="margin: 20px;"><a href="@condition.copy(milestone = Some(Some(milestone))).toURL" class="username"><i class="octicon octicon-milestone"></i>
@milestone</a></span>
}
</div>
</td>
</th>
</tr>
}
</thead>
<tbody>
@if(issues.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
@if(target == "issues"){
No issues to show.
} else {
No pull requests to show.
}
@if(condition.labels.nonEmpty || condition.milestone.isDefined){
<a href="@condition.copy(labels = Set.empty, milestone = None).toURL">Clear active filters.</a>
} else {
@if(repository.isDefined){
@if(target == "issues"){
<a href="@url(repository.get)/issues/new">Create a new issue.</a>
} else {
<a href="@url(repository.get)/compare">Create a new pull request.</a>
}
}
}
</td>
</tr>
}
@issues.map { case IssueInfo(issue, labels, milestone, commentCount, commitStatus) =>
<tr>
<td style="padding-top: 12px; padding-bottom: 12px;">
@if(hasWritePermission){
<input type="checkbox" value="@issue.issueId"/>
}
<i class="octicon octicon-issue-@(if(issue.closed) "closed" else "opened")" style="margin-right: 3px;"></i>
@if(repository.isEmpty){
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a>&nbsp;&#xFF65;
}
@if(target == "issues"){
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
}
@commitstatus(issue, commitStatus)
@labels.map { label =>
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
}
<span class="pull-right small">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20, tooltip = true)
}
@if(commentCount > 0){
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">
<i class="octicon octicon-comment active"></i> @commentCount
</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count" style="color: silver;">
<i class="octicon octicon-comment"></i> @commentCount
</a>
}
</span>
<div class="small muted" style="margin-left: 40px; margin-top: 2px;">
#@issue.issueId opened @helper.html.datetimeago(issue.registeredDate) by @user(issue.openedUserName, styleClass="username")
@milestone.map { milestone =>
<span style="margin: 20px;"><a href="@condition.copy(milestone = Some(Some(milestone))).toURL" class="username"><i class="octicon octicon-milestone"></i> @milestone</a></span>
}
</div>
</td>
</tr>
}
</tbody>
</table>
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), gitbucket.core.service.IssuesService.IssueLimit, 10, condition.toURL)

View File

@@ -13,16 +13,16 @@
}
<hr style="margin-top: 12px; margin-bottom: 18px;" class="fill-width"/>
<form method="POST" action="@url(repository)/issues/milestones/@if(milestone.isEmpty){new}else{@milestone.get.milestoneId/edit}" validate="true">
<fieldset>
<input type="text" id="title" name="title" style="width: 500px;" value="@milestone.map(_.title)" placeholder="Title"/>
<fieldset class="form-group">
<input type="text" id="title" name="title" class="form-control" style="width: 500px;" value="@milestone.map(_.title)" placeholder="Title"/>
<span id="error-title" class="error"></span>
</fieldset>
<fieldset>
<fieldset class="form-group">
<label for="description" class="strong">Description</label>
<textarea id="description" name="description" style="width: 500px; height: 150px;">@milestone.map(_.description)</textarea>
<textarea id="description" name="description" class="form-control" style="width: 500px; height: 150px;">@milestone.map(_.description)</textarea>
<span id="error-description" class="error"></span>
</fieldset>
<fieldset>
<fieldset class="form-group">
<label for="dueDate" class="strong">Due Date</label>
@helper.html.datepicker("dueDate", milestone.flatMap(_.dueDate))
<span id="error-dueDate" class="error"></span>
@@ -30,16 +30,16 @@
<hr>
<div class="pull-right">
@if(milestone.isEmpty){
<input type="submit" class="btn" value="Create milestone"/>
<input type="submit" class="btn btn-default" value="Create milestone"/>
} else {
@if(milestone.get.closedDate.isDefined){
<input type="button" class="btn" value="Open" id="open"
<input type="button" class="btn btn-default" value="Open" id="open"
onclick="location.href='@url(repository)/issues/milestones/@milestone.get.milestoneId/close';"/>
} else {
<input type="button" class="btn" value="Close" id="close"
<input type="button" class="btn btn-default" value="Close" id="close"
onclick="location.href='@url(repository)/issues/milestones/@milestone.get.milestoneId/open';"/>
}
<input type="submit" class="btn" value="Update milestone"/>
<input type="submit" class="btn btn-default" value="Update milestone"/>
}
</div>
</form>

View File

@@ -9,89 +9,99 @@
@issues.html.navigation("milestones", hasWritePermission, repository)
<br>
<table class="table table-bordered table-hover table-issues">
<tr>
<th style="background-color: #eee;">
<span class="small">
<a class="button-link@if(state == "open"){ selected}" href="?state=open">
<i class="octicon octicon-milestone @(if(state == "open"){"active"})"></i>
@milestones.count(_._1.closedDate.isEmpty) Open
</a>&nbsp;&nbsp;
<a class="button-link@if(state == "closed"){ selected}" href="?state=closed">
<i class="octicon octicon-milestone @(if(state == "closed"){"active"})"></i>
@milestones.count(_._1.closedDate.isDefined) Closed
</a>
</span>
</th>
</tr>
@defining(milestones.filter { case (milestone, _, _) =>
milestone.closedDate.map(_ => state == "closed").getOrElse(state == "open")
}){ milestones =>
@milestones.map { case (milestone, openCount, closedCount) =>
<thead>
<tr>
<td style="padding-top: 15px; padding-bottom: 15px;">
<div class="milestone row-fluid">
<div class="span4">
<a href="@url(repository)/issues?milestone=@milestone.title&state=open" class="milestone-title">@milestone.title</a>
<div style="margin-top: 6px">
@if(milestone.closedDate.isDefined){
<span class="muted">Closed @helper.html.datetimeago(milestone.closedDate.get)</span>
} else {
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<i class="octicon octicon-alert" style="color:#BD2C00;"></i><span class="muted 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>
</div>
<div class="span8">
@progress(openCount + closedCount, closedCount)
<div>
<th style="background-color: #eee;">
<span class="small">
<a class="button-link@if(state == "open"){ selected}" href="?state=open">
<i class="octicon octicon-milestone @(if(state == "open"){"active"})"></i>
@milestones.count(_._1.closedDate.isEmpty) Open
</a>&nbsp;&nbsp;
<a class="button-link@if(state == "closed"){ selected}" href="?state=closed">
<i class="octicon octicon-milestone @(if(state == "closed"){"active"})"></i>
@milestones.count(_._1.closedDate.isDefined) Closed
</a>
</span>
</th>
</tr>
</thead>
<tbody>
@defining(milestones.filter { case (milestone, _, _) =>
milestone.closedDate.map(_ => state == "closed").getOrElse(state == "open")
}){ milestones =>
@milestones.map { case (milestone, openCount, closedCount) =>
<tr>
<td style="padding-top: 15px; padding-bottom: 15px;">
<div class="milestone row">
<div class="col-md-4">
<a href="@url(repository)/issues?milestone=@milestone.title&state=open" class="milestone-title">@milestone.title</a>
<div>
@if(closedCount == 0){
0%
@if(milestone.closedDate.isDefined){
<span class="muted">Closed @helper.html.datetimeago(milestone.closedDate.get)</span>
} else {
@((closedCount.toDouble / (openCount + closedCount).toDouble * 100).toInt)%
} <span class="muted">complete</span> &nbsp;&nbsp;
@openCount <span class="muted">open</span> &nbsp;&nbsp;
@closedCount <span class="muted">closed</span>
</div>
<div class="milestone-menu">
@if(hasWritePermission){
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/edit">Edit</a> &nbsp;&nbsp;
@if(milestone.closedDate.isDefined){
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/open">Open</a> &nbsp;&nbsp;
} else {
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/close">Close</a> &nbsp;&nbsp;
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<i class="octicon octicon-alert" style="color:#BD2C00;"></i><span class="muted milestone-alert">Due by @date(dueDate)</span>
} else {
<span class="muted">Due by @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/delete" class="delete">Delete</a>
}
</div>
</div>
<div class="col-md-8">
@progress(openCount + closedCount, closedCount)
<div>
<div>
@if(closedCount == 0){
0%
} else {
@((closedCount.toDouble / (openCount + closedCount).toDouble * 100).toInt)%
} <span class="muted">complete</span> &nbsp;&nbsp;
@openCount <span class="muted">open</span> &nbsp;&nbsp;
@closedCount <span class="muted">closed</span>
</div>
<div class="milestone-menu">
@if(hasWritePermission){
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/edit">Edit</a> &nbsp;&nbsp;
@if(milestone.closedDate.isDefined){
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/open">Open</a> &nbsp;&nbsp;
} else {
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/close">Close</a> &nbsp;&nbsp;
}
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/delete" class="delete">Delete</a>
}
</div>
</div>
</div>
</div>
</div>
@if(milestone.description.isDefined){
<div class="milestone-description markdown-body">
@markdown(milestone.description.get, repository, false, false)
</div>
}
</td>
</tr>
}
@if(milestones.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
No milestones to show.
@if(hasWritePermission){
<a href="@url(repository)/issues/milestones/new">Create a new milestone.</a>
}
</td>
</tr>
}
@if(milestone.description.isDefined){
<div class="milestone-description markdown-body">
@markdown(
markdown = milestone.description.get,
repository = repository,
enableWikiLink = false,
enableRefsLink = false,
enableLineBreaks = true
)
</div>
}
</td>
</tr>
}
@if(milestones.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
No milestones to show.
@if(hasWritePermission){
<a href="@url(repository)/issues/milestones/new">Create a new milestone.</a>
}
</td>
</tr>
}
</tbody>
}
</table>
}

View File

@@ -1,6 +1,5 @@
@(total: Int, progress: Int)
<div class="milestone-progress">
@if(progress > 0){
<span class="milestone-progress" style="width: @((progress.toDouble / total.toDouble * 100).toInt)%;"></span>
}
<div class="progress" style="height: 12px; margin-bottom: 8px;">
<div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="@((progress.toDouble / total.toDouble * 100).toInt)" aria-valuemin="0" aria-valuemax="100" style="width: @((progress.toDouble / total.toDouble * 100).toInt)%">
</div>
</div>

View File

@@ -4,55 +4,53 @@
condition: Option[gitbucket.core.service.IssuesService.IssueSearchCondition] = None)(implicit context: gitbucket.core.controller.Context)
@import context._
@import gitbucket.core.view.helpers._
<ul class="nav nav-pills-group pull-left fill-width">
<li class="@if(active == "issues" ){active} first"><a href="@url(repository)/issues">Issues</a></li>
<ul class="nav nav-pills pull-left" style="line-height: 14px;">
<li class="@if(active == "issues" ){active}"><a href="@url(repository)/issues">Issues</a></li>
<li class="@if(active == "pulls" ){active}"><a href="@url(repository)/pulls">Pull requests</a></li>
<li class="@if(active == "labels" ){active}"><a href="@url(repository)/issues/labels">Labels</a></li>
<li class="@if(active == "milestones"){active} last"><a href="@url(repository)/issues/milestones">Milestones</a></li>
<li class="pull-right">
<form method="GET" id="search-filter-form" style="margin-bottom: 0px;">
@condition.map { condition =>
@if(loginAccount.isDefined){
<div class="input-prepend" style="margin-bottom: 0px;">
<div class="btn-group">
<button type="button" class="btn dropdown-toggle" data-toggle="dropdown" style="height: 34px;">
Filter
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a href="?q=is:open">Open issues and pull requests</a></li>
<li><a href="?q=is:open+is:issue+author:@urlEncode(loginAccount.get.userName)">Your issues</a></li>
<li><a href="?q=is:open+is:pr+author:@urlEncode(loginAccount.get.userName)">Your pull requests</a></li>
<li><a href="?q=is:open+assignee:@urlEncode(loginAccount.get.userName)">Everything assigned to you</a></li>
@*
<li><a href="?q=is:open+mentions:@urlEncode(loginAccount.get.userName)">Everything mentioning you</a></li>
*@
</ul>
</div>
<input type="text" id="search-filter-box" class="input-xlarge" name="q" style="height: 24px;" value="is:@{if(active == "issues") "issue" else "pr"} @condition.toFilterString"/>
</div>
} else {
<input type="text" id="search-filter-box" class="input-xlarge" name="q" style="height: 24px;" value="is:@{if(active == "issues") "issue" else "pr"} @condition.toFilterString"/>
}
}
@if(loginAccount.isDefined){
<div class="btn-group">
@if(newButton){
@if(active == "issues"){
<a class="btn btn-success" href="@url(repository)/issues/new" style="height: 24px;">New issue</a>
}
@if(active == "pulls"){
<a class="btn btn-success" href="@url(repository)/compare" style="height: 24px;">New pull request</a>
}
@if(active == "labels"){
<a class="btn btn-success" href="javascript:void(0);" id="new-label-button" style="height: 24px;">New label</a>
}
@if(active == "milestones"){
<a class="btn btn-success" href="@url(repository)/issues/milestones/new" style="height: 24px;">New milestone</a>
}
}
</div>
}
</form>
</li>
<li class="@if(active == "milestones"){active}"><a href="@url(repository)/issues/milestones">Milestones</a></li>
</ul>
<form method="GET" id="search-filter-form" style="margin-bottom: 0px;" class="form-inline pull-right">
@condition.map { condition =>
@if(loginAccount.isDefined){
<div class="form-group" style="width: 300px">
<div class="input-group" style="margin-bottom: 0px;">
<div class="input-group-btn">
<button type="button" class="btn btn-default btn-lg dropdown-toggle" data-toggle="dropdown">
Filter
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a href="?q=is:open">Open issues and pull requests</a></li>
<li><a href="?q=is:open+is:issue+author:@urlEncode(loginAccount.get.userName)">Your issues</a></li>
<li><a href="?q=is:open+is:pr+author:@urlEncode(loginAccount.get.userName)">Your pull requests</a></li>
<li><a href="?q=is:open+assignee:@urlEncode(loginAccount.get.userName)">Everything assigned to you</a></li>
@*
<li><a href="?q=is:open+mentions:@urlEncode(loginAccount.get.userName)">Everything mentioning you</a></li>
*@
</ul>
</div>
<input type="text" id="search-filter-box" class="form-control input-lg" size="40" name="q" value="is:@{if(active == "issues") "issue" else "pr"} @condition.toFilterString"/>
</div>
</div>
} else {
<input type="text" id="search-filter-box" class="form-control input-lg" size="40" name="q" value="is:@{if(active == "issues") "issue" else "pr"} @condition.toFilterString"/>
}
}
@if(loginAccount.isDefined){
@if(newButton){
@if(active == "issues"){
<a class="btn btn-success btn-lg" href="@url(repository)/issues/new">New issue</a>
}
@if(active == "pulls"){
<a class="btn btn-success btn-lg" href="@url(repository)/compare">New pull request</a>
}
@if(active == "labels"){
<a class="btn btn-success btn-lg" href="javascript:void(0);" id="new-label-button">New label</a>
}
@if(active == "milestones"){
<a class="btn btn-success btn-lg" href="@url(repository)/issues/milestones/new">New milestone</a>
}
}
}
</form>

View File

@@ -11,10 +11,12 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<link rel="icon" href="@assets/common/images/gitbucket.png" type="image/vnd.microsoft.icon" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="@assets/vendors/bootstrap/css/bootstrap.css" rel="stylesheet">
<link href="@assets/vendors/bootstrap/css/bootstrap-responsive.css" rel="stylesheet">
<link href="@assets/vendors/bootstrap-theme-github-3.1.1/css/bootstrap.css" rel="stylesheet">
@*
<link href="@assets/vendors/bootstrap-theme-github-3.1.1/css/bootstrap-responsive.css" rel="stylesheet">
*@
<link href="@assets/vendors/octicons/octicons.css" rel="stylesheet">
<link href="@assets/vendors/datepicker/css/datepicker.css" rel="stylesheet">
<link href="@assets/vendors/datepicker/css/bootstrap-datetimepicker.min.css" rel="stylesheet">
<link href="@assets/vendors/colorpicker/css/bootstrap-colorpicker.css" rel="stylesheet">
<link href="@assets/vendors/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/>
<link href="@assets/vendors/facebox/facebox.css" rel="stylesheet"/>
@@ -23,8 +25,10 @@
<script src="@assets/vendors/dropzone/dropzone.js"></script>
<script src="@assets/common/js/validation.js"></script>
<script src="@assets/common/js/gitbucket.js"></script>
<script src="@assets/vendors/bootstrap/js/bootstrap.js"></script>
<script src="@assets/vendors/datepicker/js/bootstrap-datepicker.js"></script>
<script src="@assets/vendors/bootstrap-theme-github-3.1.1/js/bootstrap.js"></script>
<script src="@assets/vendors/bootstrap3-typeahead/bootstrap3-typeahead.js"></script>
<script src="@assets/vendors/datepicker/js/moment.js"></script>
<script src="@assets/vendors/datepicker/js/bootstrap-datetimepicker.min.js"></script>
<script src="@assets/vendors/colorpicker/js/bootstrap-colorpicker.js"></script>
<script src="@assets/vendors/google-code-prettify/prettify.js"></script>
<script src="@assets/vendors/zclip/ZeroClipboard.min.js"></script>
@@ -38,58 +42,69 @@
}
</head>
<body>
<form id="search" action="@path/search" method="POST">
<div class="navbar">
<div class="navbar-inner">
<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>
<span class="icon-bar"></span>
</button>
<a class="brand" href="@path/">
<img src="@assets/common/images/gitbucket.png" style="width: 24px; height: 24px;"/>GitBucket
@defining(AutoUpdate.getCurrentVersion){ version =>
<span class="header-version">@version.majorVersion.@version.minorVersion</span>
}
</a>
<form id="search" action="@path/search" method="POST" class="form-inline">
<nav class="navbar navbar-default">
<div class="container" style="width: 980px;">
@* TODO: for plugi-ins?
<button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
*@
<a class="navbar-brand" href="@path/">
<img src="@assets/common/images/gitbucket.png" style="width: 24px; height: 24px;"/>GitBucket
@defining(AutoUpdate.getCurrentVersion){ version =>
<span class="header-version">@version.majorVersion.@version.minorVersion</span>
}
</a>
@if(loginAccount.isDefined){
@repository.map { repository =>
<input type="text" name="query" style="width: 200px; margin-top: 5px; margin-bottom: 0px;" placeholder="Search this repository"/>
<input type="text" name="query" class="form-control" style="width: 200px; margin-bottom: 0px;" placeholder="Search this repository"/>
<input type="hidden" name="owner" value="@repository.owner"/>
<input type="hidden" name="repository" value="@repository.name"/>
}
@if(loginAccount.isDefined){
<a href="@path/dashboard/pulls" class="global-header-menu">Pull requests</a>
<a href="@path/dashboard/issues" class="global-header-menu">Issues</a>
<a href="@path/dashboard/pulls" class="global-header-menu">Pull requests</a>
<a href="@path/dashboard/issues" class="global-header-menu">Issues</a>
} else {
@* TODO: merge with below *@
@repository.map { repository =>
<input type="text" name="query" class="form-control" style="width: 200px; margin-top: 6px; margin-bottom: 0px;" placeholder="Search this repository"/>
<input type="hidden" name="owner" value="@repository.owner"/>
<input type="hidden" name="repository" value="@repository.name"/>
}
<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: 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 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>
}
<li><a href="@path/signout">Sign out</a></li>
</ul>
</div>
} else {
<a href="@path/signin?redirect=@urlEncode(currentPath)" class="btn" id="signin">Sign in</a>
}
</div><!--/.nav-collapse -->
</div>
}
@if(loginAccount.isDefined){
<div class="pull-right" style="margin-top: 6px;">
<div class="btn-group" style="margin-right: 8px;">
<a class="dropdown-toggle menu" data-toggle="dropdown" href="#">
<i class="octicon octicon-plus" style="color: black; font-size: 20px; vertical-align: middle;height:20px !important;"></i>
<span class="caret" style="color: black; 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">
<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="color: black; vertical-align: middle;"></span>
</a>
<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>
}
<li><a href="@path/signout">Sign out</a></li>
</ul>
</div>
</div>
} else {
<a href="@path/signin?redirect=@urlEncode(currentPath)" class="btn btn-default pull-right" style="margin-top: 6px;" id="signin">Sign in</a>
}
</div>
</div>
</nav>
</form>
@body
<script>

View File

@@ -24,38 +24,42 @@
<div class="container">
@helper.html.information(info)
@helper.html.error(error)
@if(repository.commitCount > 0){
<div class="pull-right">
<div class="input-prepend">
@if(loginAccount.isEmpty){
<a title="You must be signed in to fork a repository" href="@path/signin?redirect=@urlEncode(s"${path}/${repository.owner}/${repository.name}")" class="btn btn-small" style="margin-bottom: 10px;">Fork</a>
} else {
@if(isNoGroup) {
<a id="fork-link" href="javascript:void(0);" class="btn btn-small" style="margin-bottom: 10px;">Fork</a>
} else {
<a href="@path/@repository.owner/@repository.name/fork" class="btn btn-small" rel="facebox" style="margin-bottom: 10px;">Fork</a>
}
<div class="row">
<div class="head">
@if(repository.commitCount > 0){
<div class="input-group pull-right">
<span class="fork">
<span class="input-group-btn">
@if(loginAccount.isEmpty){
<a title="You must be signed in to fork a repository" href="@path/signin?redirect=@urlEncode(s"${path}/${repository.owner}/${repository.name}")" class="btn btn-default">Fork</a>
} else {
@if(isNoGroup) {
<a id="fork-link" href="javascript:void(0);" class="btn btn-default">Fork</a>
} else {
<a href="@path/@repository.owner/@repository.name/fork" class="btn btn-default" rel="facebox">Fork</a>
}
}
</span>
<span class="count"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
</span>
</div>
@if(loginAccount.isDefined && isNoGroup){
<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>
}
}
@helper.html.repositoryicon(repository, true)
<a href="@url(repository.owner)">@repository.owner</a> / <a href="@url(repository)" class="strong">@repository.name</a>
@defining(repository.repository){ x =>
@if(repository.repository.originRepositoryName.isDefined){
<div class="forked">
forked from <a href="@path/@x.parentUserName/@x.parentRepositoryName">@x.parentUserName/@x.parentRepositoryName</a>
</div>
}
<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" style="display: none;">
<input type="hidden" name="account" value="@loginAccount.get.userName"/>
</form>
}
</div>
}
<div class="head">
@helper.html.repositoryicon(repository, true)
<a href="@url(repository.owner)">@repository.owner</a> / <a href="@url(repository)" class="strong">@repository.name</a>
@defining(repository.repository){ x =>
@if(repository.repository.originRepositoryName.isDefined){
<div class="forked">
forked from <a href="@path/@x.parentUserName/@x.parentRepositoryName">@x.parentUserName/@x.parentRepositoryName</a>
</div>
}
}
</div>
</div>
<hr style="margin-bottom: 20px;"/>
@@ -77,7 +81,7 @@
<strong id="repository-url-proto">HTTP</strong> <span class="mute">clone URL</span>
</div>
@helper.html.copy("repository-url-copy", repository.httpUrl){
<input type="text" value="@repository.httpUrl" id="repository-url" readonly>
<input type="text" value="@repository.httpUrl" id="repository-url" class="form-control input-sm" readonly>
}
@if(settings.ssh && loginAccount.isDefined){
<div class="small">
@@ -87,26 +91,26 @@
@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>
<a href="@repository.httpOpenRepoUrl(context.platform)" id="repository-clone-url" class="btn btn-sm btn-default btn-block"><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>
<a href="@{url(repository)}/archive/@{encodeRefName(id)}.zip" class="btn btn-sm btn-default btn-block"><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>
<a href="@{url(repository)}/archive/@{encodeRefName(id)}.tar.gz" class="btn btn-sm btn-default btn-block "><i class="octicon octicon-cloud-download"></i>Download TAR.GZ</a>
</div>
*@
}
}
</div>
<div style="margin-right: @if(expand){180px} else {50px};">
<div style="margin-right: @if(expand){180px} else {55px};">
@if(expand){
@repository.repository.description.map { description =>
<p class="description">@description</p>
<p class="description">@detectAndRenderLinks(description)</p>
}
<div style="margin-bottom: 10px;" class="box-content">
<div style="margin-bottom: 10px; padding: 4px;" class="panel panel-default">
<table class="fill-width">
<tr>
<td style="width: 33%; text-align: center;">

View File

@@ -9,37 +9,37 @@
<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 class="list-group box-commits">
@day.map { commit =>
<div class="list-group-item">
<ul class="nav nav-pills pull-right">
<li><a href="@url(repository)/commit/@commit.id" class="link monospace" style="line-height: 16px;">@commit.id.substring(0, 7)</a></li>
<li><a href="@url(repository)/tree/@commit.id" style="line-height: 16px;"><i class="octicon octicon-code link"></i></a></li>
</ul>
<div>
<div class="commit-avatar-image">@avatar(commit, 40)</div>
<div>
<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 style="margin-top: 2px;">
@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>
</div>
}
}
</div>
}
</div>

View File

@@ -53,21 +53,22 @@
</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="row">
<div class="col-md-10">
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
<div class="box issue-box">
<div class="box-content">
<div class="panel panel-default issue-box">
<div class="panel-body">
<span class="error" id="error-title"></span>
<input type="text" name="title" style="width: 690px" placeholder="Title"/>
<input type="text" name="title" class="form-control input-lg" style="width: 680px" placeholder="Title"/>
@helper.html.preview(
repository = repository,
content = "",
enableWikiLink = false,
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = true,
style = "width: 690px; height: 200px;"
style = "width: 680px; height: 200px;"
)
<input type="hidden" name="targetUserName" value="@originRepository.owner"/>
<input type="hidden" name="targetBranch" value="@originId"/>
@@ -77,12 +78,12 @@
<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"/>
<input type="submit" class="btn btn-lg btn-success" value="Create pull request"/>
</div>
</div>
</div>
</div>
<div class="span2">
<div class="col-md-2">
@issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map((_, 0, 0)), labels, hasOriginWritePermission, repository)
</div>
</div>
@@ -90,6 +91,11 @@
</div>
}
@if(commits.isEmpty){
<div class="panel panel-default" style="padding: 20px; background-color: #eee; text-align: center;">
<h4>There isn't anything to compare.</h4>
<span class="strong">@originRepository.owner:@originId</span> and <span class="strong">@forkedRepository.owner:@forkedId</span> are identical.
</div>
@*
<table class="table table-bordered table-hover table-issues">
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
@@ -98,8 +104,9 @@
</td>
</tr>
</table>
*@
} else {
<div style="margin-bottom: 10px;" class="box-content">
<div style="margin-bottom: 10px; padding: 4px;" class="panel panel-default">
<table class="fill-width">
<tr>
<td style="width: 25%; text-align: center;">
@@ -167,8 +174,8 @@
$(function(){
$('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', 'octicon-check');
e.parents('ul').find('i').attr('class', 'octicon');
e.find('i').addClass('octicon-check');
e.parents('div.btn-group').find('button span.strong').text(e.text());
@if(members.isEmpty){

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