Compare commits

...

155 Commits
2.4 ... 2.8

Author SHA1 Message Date
Naoki Takezoe
0085cb24ad Add description about 2.8 2015-02-01 13:00:31 +09:00
Naoki Takezoe
6a758902ef Small fix for #615 2015-02-01 12:55:37 +09:00
Naoki Takezoe
0d81a9a9b6 Merge pull request #615 from team-lab/fix/xss-by-raw-html
fix/xss by raw html
2015-02-01 12:44:15 +09:00
Naoki Takezoe
6e4f6da633 Merge pull request #612 from team-lab/fix/update-pullrequest-on-commit-by-online-editor
fix/update pullrequest when file edited by online editor
2015-01-31 18:19:46 +09:00
Naoki Takezoe
15118ca5c1 Merge pull request #614 from HairyFotr/patch-typo
Fix typo
2015-01-31 13:58:55 +09:00
HairyFotr
8161560757 Fix typo 2015-01-30 21:34:25 +01:00
nazoking
9ba564c864 test/html is cause of xss 2015-01-30 15:32:53 +09:00
nazoking
06b5b92673 update pullrequest commitId on file edited by online editor 2015-01-30 04:14:04 +09:00
Naoki Takezoe
b9b6589bd7 Update README.md 2015-01-29 21:47:54 +09:00
Naoki Takezoe
b79f6a5fa0 Update README.md 2015-01-29 21:47:00 +09:00
Shintaro Murakami
bd046da3d0 (refs #532) Fix rendering of link over image 2015-01-28 00:09:34 +09:00
Naoki Takezoe
a889ed7c46 Merge pull request #591 from marklacroix/anon-access
(refs #274) Add option to deny anonymous (i.e. unauthorized) access
2015-01-27 10:28:11 +09:00
Naoki Takezoe
e24684cb2b Update favicon 2015-01-25 20:01:12 +09:00
Naoki Takezoe
5f939c18b4 (refs #609)Convert labelId when rename repository 2015-01-25 14:45:37 +09:00
takezoe
d412dd5009 (refs #600)Fix broken layout 2015-01-25 01:16:03 +09:00
Mark LaCroix
8643bfeb37 Merge remote-tracking branch 'upstream/master' into anon-access
Conflicts:
	src/main/scala/app/SystemSettingsController.scala
	src/main/scala/service/SystemSettingsService.scala
	src/test/scala/view/AvatarImageProviderSpec.scala
2015-01-21 15:49:42 -05:00
Naoki Takezoe
31b6adf0e5 Merge pull request #606 from bati11/fix-mergeguide-text
Fix merge guide's text
2015-01-21 01:39:37 +09:00
bati11
f1ac2b3507 Change checkout branch name from "master" to ${pullreq.branch} 2015-01-20 23:32:47 +09:00
Naoki Takezoe
135e1ef73d Merge pull request #602 from mrkm4ntr/default-privacy-option-to-create-repo
(refs #495,#595) Add configuration to set default visibility option to create new repositories.
2015-01-20 10:59:02 +09:00
Naoki Takezoe
da55bf6af3 Apply new icon 2015-01-18 14:17:41 +09:00
Naoki Takezoe
883a9c8b17 Improve Wiki rendering performance 2015-01-18 14:06:19 +09:00
Naoki Takezoe
7da89940e3 Use the issues list template for the pull request list in the dashboard 2015-01-18 03:59:33 +09:00
Shintaro Murakami
3233b0ae3c Fix failed test. 2015-01-17 23:28:08 +09:00
Shintaro Murakami
4c2ed09915 (refs #495,#595) Add configuration to set default visibility option to create new repositories. 2015-01-17 23:04:41 +09:00
Naoki Takezoe
256b6c480f Merge pull request #546 from rlazoti/add-link-to-dashboard
Add repository's link to issue and pull request list on dashboard
2015-01-17 16:44:04 +09:00
Naoki Takezoe
dc311837f9 Merge pull request #596 from tnayuki/streaming-archive
not to make a temporary file when archive
2015-01-17 16:29:36 +09:00
Naoki Takezoe
92aec48c99 Merge pull request #547 from mslinn/master
Change Bootstrap's default color pink for code tags to match github's color
2015-01-17 16:17:43 +09:00
Naoki Takezoe
a6ada8c457 Merge pull request #582 from team-lab/add-message-to-login
Show information message on singin view.
2015-01-17 14:50:37 +09:00
Shintaro Murakami
dcc601502e (refs #589) Prevent adding event handler several times 2015-01-14 23:04:23 +09:00
Shintaro Murakami
dd58d8c804 (refs #598) Exclude count of pull requests from that of issues. 2015-01-12 22:50:12 +09:00
Shintaro Murakami
2ade54b7e3 (#refs 549) Change "…" at skipped line to pseudo element 2015-01-11 01:04:13 +09:00
Shintaro Murakami
136c5854f3 (refs #593) Expand column size of FILE_NAME in COMIT_COMMENT 2015-01-11 00:28:52 +09:00
Shintaro Murakami
c597238d9c (refs #549) Selecting lines in diff without line numbers. 2015-01-10 01:22:47 +09:00
Toru Nayuki
2552a58e08 not to make a temporary file when archive 2015-01-09 18:39:48 +09:00
nazoking
74ad5872a3 Revert "add information to singup view"
This reverts commit f7fd53bf09.
2015-01-09 14:55:28 +09:00
Shintaro Murakami
485d502bd3 (refs #584) Refactored 2015-01-09 00:16:09 +09:00
Naoki Takezoe
47bc8d030e Merge pull request #590 from ghmer/master
Allow LDAPS connections instead of only allowing TLS enabled connections
2015-01-08 02:44:36 +09:00
Mark LaCroix
48fe7133f7 Add anonymous access option to tests 2015-01-07 09:47:36 -05:00
Mark LaCroix
5d962dc5e4 Add option to deny anonymous (i.e. unauthorized) access 2015-01-07 09:17:22 -05:00
Mario Enrico Ragucci
31e8e5a951 code alignment. We want a pretty pull request! 2015-01-07 07:46:59 +01:00
Mario Enrico Ragucci
858373c628 small beautifying change to have code properly aligned 2015-01-07 07:45:18 +01:00
Mario Enrico Ragucci
7f142d2c0d Introducing "Enable SSL" option on LDAP settings 2015-01-07 07:41:41 +01:00
Shintaro Murakami
08b86232a8 Merge pull request #589 from mrkm4ntr/toggle-line-notes
Add checkbox to toggle inline notes.
2015-01-06 23:48:11 +09:00
Shintaro Murakami
6bf4f42fdb Add checkbox to toggle line notes 2015-01-06 23:27:03 +09:00
Shintaro Murakami
f3c7de36d8 Remove filter setting for old plugin 2015-01-05 22:28:30 +09:00
Naoki Takezoe
19f556de57 Merge pull request #587 from mrkm4ntr/comment-for-split-diff
(refs #564) Comment for side-by-side diff available
2015-01-04 13:46:33 +09:00
Naoki Takezoe
e4467df411 Merge pull request #586 from team-lab/feature/add-stat-icon-on-diff
add icon on each diff header
2015-01-04 13:25:15 +09:00
Shintaro Murakami
8d305a1fb1 Merge branch 'master' of https://github.com/takezoe/gitbucket into comment-for-split-diff 2015-01-04 10:47:51 +09:00
Shintaro Murakami
b47153e645 Remove old plugin test 2015-01-04 10:40:10 +09:00
Shintaro Murakami
c71766c84b (refs #564) Comment for side-by-side diff available 2015-01-03 17:33:33 +09:00
Naoki Takezoe
23e4d679ae Merge branch 'purge-old-plugin-system' 2015-01-03 04:47:10 +09:00
Naoki Takezoe
182acb2e02 Trim each lines of command guidance 2015-01-02 19:11:50 +09:00
nazoking
b255b15006 add icon on diff view 2015-01-02 17:35:53 +09:00
Naoki Takezoe
b458f88161 Remove enable.plugin flag 2015-01-02 02:27:35 +09:00
Naoki Takezoe
398d8f2f1c Merge branch 'master' into purge-old-plugin-system 2015-01-02 02:03:00 +09:00
Naoki Takezoe
85c1a56cbf Purge old plugin system 2015-01-02 01:59:21 +09:00
Shintaro Murakami
da216c6960 (refs #585) Fix issue in markdown preview 2014-12-31 16:24:30 +09:00
Naoki Takezoe
bc91b153bf Merge pull request #574 from michaeljayt/add-fork-options
Add fork to group options
2014-12-31 01:08:27 +09:00
Shintaro Murakami
bc50b47d3a (refs #584) Fix the activity of commenting to pull request. 2014-12-31 00:27:47 +09:00
michaeljayt
aed15a7f25 Skip the group popup when user has no group 2014-12-30 14:26:30 +08:00
michaeljayt
a1f09117b0 Fix security issue on fork 2014-12-30 08:50:19 +08:00
michaeljayt
0a4a4a51ca Add fork to group options 2014-12-30 08:50:19 +08:00
nazoking
f7fd53bf09 add information to singup view 2014-12-29 20:54:22 +09:00
nazoking
cbfb863a54 Add information message to singin view. 2014-12-29 19:56:52 +09:00
Naoki Takezoe
9d56d72611 Update README.md for 2.7 release 2014-12-29 11:32:23 +09:00
Naoki Takezoe
527c91ff9d Fix for #581. Column name and CSS is changed. 2014-12-29 03:24:04 +09:00
Naoki Takezoe
c58c2d6700 Merge pull request #556 from michaeljayt/add-ssh-clone-url
Add SSH clone url option when enabled SSH access
2014-12-29 02:12:18 +09:00
Naoki Takezoe
5518eca952 Merge pull request #581 from mrkm4ntr/fix-comment-in-pr
Fix bug on showing inline comments in Pull Request.
2014-12-29 02:10:57 +09:00
Shintaro Murakami
6e2b67ec0b Fix bug on showing inline comments in Pull Request. 2014-12-28 20:12:32 +09:00
Naoki Takezoe
837b1e44a7 Merge pull request #561 from torutk/rpm_nonroot
Enable contrib init file and RPM's spec file to run on RHEL 6/CentOS 6 a...
2014-12-27 20:55:26 +09:00
Naoki Takezoe
e04c230c6e Merge pull request #580 from HairyFotr/patch-lint
Fix a few issues detected by static analysis
2014-12-27 20:10:02 +09:00
HairyFotr
a01b5a4a59 Fix a few issues detected by static analysis 2014-12-26 15:40:05 +01:00
Naoki Takezoe
427b6ce846 Merge pull request #579 from banjun/fix-editor-preview-NoSuchElementException-enableTaskList
fix repo editor fails to preview
2014-12-22 23:06:46 +09:00
banjun
b7b5af2b72 add enableTaskList to post params for _preview 2014-12-22 19:36:12 +09:00
Naoki Takezoe
39fec57f72 (refs #578)Add migration for repositories which have removed parent or origin repository. 2014-12-22 01:32:15 +09:00
Naoki Takezoe
238dedb6df (refs #578)Clean original and parent repository information when it's deleted 2014-12-21 21:39:20 +09:00
Naoki Takezoe
af091117b7 (refs #577)Remove all HTML tags in Markdown 2014-12-20 01:51:15 +09:00
michaeljayt
ddea4e12f0 Add SSH clone url option when enabled SSH access 2014-12-12 21:58:21 +08:00
Naoki Takezoe
9767903252 (refs #567)Fix condition of repository search for issues. 2014-12-05 02:10:28 +09:00
takezoe
bc75f9f8a2 (refs #564)Fix for repository renaming 2014-11-28 01:42:44 +09:00
takezoe
63627fc1d0 (refs #564)Change the attached files directory to /commens from /issues 2014-11-28 00:20:46 +09:00
Naoki Takezoe
c23985c1a7 Merge pull request #564 from mrkm4ntr/coment-for-diff
(refs #9) Comments for commit and diff
2014-11-28 00:08:59 +09:00
Shintaro Murakami
af58e99dcf (refs #9) Comments for commit and diff 2014-11-26 22:59:52 +09:00
Toru Takahashi
676670e9e3 Enable contrib init file and RPM's spec file to run on RHEL 6/CentOS 6 as non root user. 2014-11-24 11:51:36 +09:00
Naoki Takezoe
823c52e941 Update README.md 2014-11-24 03:18:10 +09:00
Naoki Takezoe
7f42007648 (refs #507)Small fix about pull request UI 2014-11-23 14:59:03 +09:00
Naoki Takezoe
7214ef21d2 (refs #559)Fix merged message 2014-11-23 11:45:22 +09:00
Naoki Takezoe
18a4492975 Update README.md 2014-11-23 11:19:24 +09:00
Naoki Takezoe
99f73b1016 (refs #560)Replace build status badge with Travis 2014-11-23 01:29:38 +09:00
Naoki Takezoe
0c1ce6a088 (refs #560)Add travis configuration 2014-11-23 01:19:26 +09:00
Naoki Takezoe
ae6291ab83 Update version to 2.6 2014-11-22 22:36:27 +09:00
Naoki Takezoe
617fcf7c99 Fix compilation error in test 2014-11-22 22:34:27 +09:00
Naoki Takezoe
9df4a74837 (refs #507)Applying new UI to pull request detail page has been completed. 2014-11-22 22:30:44 +09:00
Naoki Takezoe
966d4251be (refs #507)Applying new UI to pull request detail page 2014-11-22 22:02:33 +09:00
Naoki Takezoe
84b2e9cdcd Fix compilation error 2014-11-22 21:47:34 +09:00
Naoki Takezoe
e29d63c91a Merge branch 'master' into newui-for-pullreq 2014-11-22 21:46:51 +09:00
Naoki Takezoe
805d2b8e79 (refs #530)Don't re-sort activities by repository renaming. 2014-11-22 21:40:32 +09:00
Naoki Takezoe
9983fd1292 Update README.md 2014-11-22 12:57:33 +09:00
Naoki Takezoe
1de202e927 (refs #554)Search box bug fix 2014-11-19 07:17:37 +09:00
Naoki Takezoe
4eb9f4a485 (refs #551)Adjust wiki buttons 2014-11-17 17:08:51 +09:00
Naoki Takezoe
a8801e4e41 (refs #432)Show the information message at the top page 2014-11-17 00:52:51 +09:00
Naoki Takezoe
ee1c84dbf2 (refs #508)Remove filter parameter 2014-11-16 20:22:36 +09:00
Naoki Takezoe
e40e1fa6cd (refs #508)Add search filter box to the dashboard 2014-11-16 01:38:31 +09:00
Naoki Takezoe
055f648ea2 (refs #508)Remove repository filter 2014-11-11 13:10:07 +09:00
Naoki Takezoe
37a399c3a2 (refs #508)Fix line separator 2014-11-11 12:09:48 +09:00
Naoki Takezoe
bc0b11b60a (refs #508)Basic filter box implementation 2014-11-11 12:07:22 +09:00
Naoki Takezoe
65a1ca7146 (refs #508)Start to add search filter box 2014-11-11 03:19:51 +09:00
Naoki Takezoe
2293030d4e Merge pull request #541 from rlazoti/fix-dashboard
Fix pull request's view on dashboard
2014-11-09 00:21:39 +09:00
Mike Slinn
2848f07b83 Merge remote-tracking branch 'upstream/master' 2014-11-08 04:11:55 -08:00
Mike Slinn
55224ddcd8 Changed Bootstrap's default color pink for code tags to match github's color 2014-11-08 04:07:14 -08:00
Rodrigo Lazoti
054ae75b6b Add repository's link to issues and pull request list on dashboard 2014-11-07 10:55:08 -02:00
Rodrigo Lazoti
c83fab611e Remove the 'All' tab 2014-11-06 19:01:16 -02:00
Rodrigo Lazoti
29baf1223c Fix pull request's view on dashboard 2014-11-04 18:15:07 -02:00
Naoki Takezoe
2a60f607ff Update README.md for 2.5 2014-11-04 01:38:29 +09:00
Naoki Takezoe
78f4d26aa0 Add version 2.5 2014-11-03 05:46:32 +09:00
Naoki Takezoe
f59e86f5ca (refs #529)Add icons 2014-11-03 05:25:03 +09:00
Naoki Takezoe
1c2af36c92 (refs #529)Mentioned filter 2014-11-03 04:35:41 +09:00
takezoe
badbe73f4e Fix for Firefox 2014-11-03 02:30:19 +09:00
takezoe
a9d58698cd (refs #529)Adjust bottom line 2014-11-03 01:40:47 +09:00
Naoki Takezoe
bb3f086aa6 (refs #529)Enhance dashboard header 2014-11-03 00:31:34 +09:00
Naoki Takezoe
2db674bb03 (refs #529)Organization filter 2014-11-02 13:26:46 +09:00
Naoki Takezoe
4bc4a16a80 Merge branch 'master' into newui-for-dashboard
Conflicts:
	src/main/twirl/dashboard/issueslist.scala.html
	src/main/twirl/dashboard/pullslist.scala.html
2014-11-01 03:14:19 +09:00
Naoki Takezoe
d88a105628 Merge pull request #512 from mrkm4ntr/create-branch-ui
(refs #394) Create branch from Web UI
2014-11-01 03:10:38 +09:00
Naoki Takezoe
15d0c5b506 Merge pull request #526 from mrkm4ntr/datetime-format
(ref #519) Change datetime formats
2014-11-01 03:10:18 +09:00
Naoki Takezoe
dbde79d2f2 Merge pull request #342 from bati11/feature-tasklist
Implement "Task List" in markdown
2014-11-01 03:09:24 +09:00
Naoki Takezoe
e6e3786b47 (refs #529)Visibility filter 2014-11-01 03:05:52 +09:00
Tomofumi Tanaka
4c1b8004fc (refs #533)Admin user must not disable self account yourself 2014-10-29 09:15:20 +09:00
shimamoto
ff4052f097 (refs #507) Fix the issue info of conversation page. 2014-10-19 23:39:25 +09:00
Naoki Takezoe
13c206d068 Applying new Issues UI to dashboard 2014-10-19 21:34:12 +09:00
Shintaro Murakami
5b875d7c73 Refactored, sorry. 2014-10-19 01:22:31 +09:00
Shintaro Murakami
e33dd9008b (ref #519) Change datetime formats 2014-10-18 23:21:47 +09:00
Naoki Takezoe
8764910553 (refs #504)Fix word-break of "Pages" table 2014-10-18 19:26:11 +09:00
Naoki Takezoe
4c89c40944 (refs #522)Recover user filter in dashboard 2014-10-18 19:11:30 +09:00
Shintaro Murakami
0f0986afcf (refs #394)Create branch from Web UI 2014-10-16 22:03:49 +09:00
Shintaro Murakami
5d5f1f8bdd (refs #514) Fix problems of renaming repository. 2014-10-16 22:03:49 +09:00
Naoki Takezoe
03e386b3ce Merge pull request #515 from mrkm4ntr/hotfix-514
(refs #514) Fix problems of renaming repository.
2014-10-12 19:05:54 +09:00
takezoe
435eac7ae6 (refs #511)Fix problem which is not possible to choose color at the colorpicker 2014-10-12 16:29:41 +09:00
takezoe
bd5df3977d (refs #518)Compile for Java7 2014-10-12 15:49:49 +09:00
bati11
ba218053f9 Modify to correspond to that "issuedetail.scala.html" has been deleted 2014-10-11 13:50:32 +09:00
bati11
1fe448a83b Merge branch 'master' into feature-tasklist
Conflicts:
	src/main/twirl/issues/issuedetail.scala.html
2014-10-11 12:07:14 +09:00
Shintaro Murakami
26a45d0117 (refs #514) Fix problems of renaming repository. 2014-10-09 22:05:42 +09:00
Naoki Takezoe
320585a530 Fix presentation problem on Firefox 2014-10-07 14:52:11 +09:00
Naoki Takezoe
ca0f888a99 Update for 2.4.1 2014-10-06 13:56:33 +09:00
Naoki Takezoe
3b08dc2e41 (refs #510)Fix dropdown presentation 2014-10-06 13:47:41 +09:00
Naoki Takezoe
cc128a49c1 (refs #510)Dirty fix for Firefox 2014-10-06 13:37:32 +09:00
Naoki Takezoe
e0148695f2 (refs #509)Fix link broken bug in Wiki 2014-10-06 13:22:42 +09:00
Mike Slinn
a10188260c Update README.md 2014-10-03 15:26:42 -07:00
bati11
26b14ded58 Add nested task list support 2014-09-20 10:57:33 +09:00
bati11
e1f310317d Modify GitBucketHtmlSerializer constructor parameters
- Add to the GitBucketHtmlSerializer constructor parameter "hasWritePermission"
- Remove the call to the RepositoryService.hasWritePermission in GitBucketHtmlSerializer
2014-09-19 14:13:53 +09:00
bati11
937814ec5d Merge branch 'master' into feature-tasklist
Conflicts:
	src/main/scala/app/IssuesController.scala
	src/main/twirl/issues/create.scala.html
2014-09-19 12:45:09 +09:00
bati11
b55fc649a6 Change crlf to lf 2014-09-19 12:43:04 +09:00
bati11
6175eb7c08 Merge branch 'master' into feature-tasklist
Conflicts:
	src/main/twirl/issues/commentform.scala.html
	src/main/twirl/issues/create.scala.html
	src/main/twirl/pulls/compare.scala.html
	src/main/twirl/wiki/edit.scala.html
2014-05-31 12:17:30 +09:00
bati11
ebb9d9329a Merge branch 'master' into feature-tasklist 2014-05-03 10:51:18 +09:00
bati11
843722f82e Implement the feature "Task List" 2014-04-10 02:08:45 +09:00
bati11
ce79eaada8 Add escapeTaskList method, it escapse '- [] ' characters 2014-04-10 01:21:55 +09:00
131 changed files with 4023 additions and 3910 deletions

3
.travis.yml Normal file
View File

@@ -0,0 +1,3 @@
language: scala
scala:
- 2.11.2

View File

@@ -1,7 +1,7 @@
GitBucket [![Gitter chat](https://badges.gitter.im/takezoe/gitbucket.png)](https://gitter.im/takezoe/gitbucket) [![Build Status](https://buildhive.cloudbees.com/job/takezoe/job/gitbucket/badge/icon)](https://buildhive.cloudbees.com/job/takezoe/job/gitbucket/)
GitBucket [![Gitter chat](https://badges.gitter.im/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 is the easily installable Github clone written with Scala.
GitBucket is the easily installable GitHub clone powered by Scala.
Features
@@ -23,7 +23,6 @@ The current version of GitBucket provides a basic features below:
Following features are not implemented, but we will make them in the future release!
- Comment for the changeset
- Network graph
- Statistics
- Watch / Star
@@ -80,6 +79,35 @@ Run the following commands in `Terminal` to
Release Notes
--------
### 2.8 - 1 Feb 2015
- New logo and icons
- New system setting options to control visibility
- Comment on side-by-side diff
- Information message on sign-in page
- Fork repository by group account
### 2.7 - 29 Dec 2014
- Comment for commit and diff
- Fix security issue in markdown rendering
- Some bug fix and improvements
### 2.6 - 24 Nov 2014
- Search box at issues and pull requests
- Information from administrator
- Pull request UI has been updated
- Move to TravisCI from Buildhive
- Some bug fix and improvements
### 2.5 - 4 Nov 2014
- New Dashboard
- Change datetime format
- Create branch from Web UI
- Task list in Markdown
- Some bug fix and improvements
### 2.4.1 - 6 Oct 2014
- Bug fix
### 2.4 - 6 Oct 2014
- New UI is applied to Issues and Pull requests
- Side-by-side diff is available

View File

@@ -8,6 +8,6 @@ Common scripts are in this directory.
This version of scripts has so far only been tested on Ubuntu and Mac. Someone else will have to test on RedHat.
To run:
1. Edit `gitbucket.conf` to suit.
2. Type: `install`
1. Edit `gitbucket.conf` to suit.
2. Type: `install`

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -44,7 +44,6 @@ object MyBuild extends Build {
"org.apache.sshd" % "apache-sshd" % "0.11.0",
"com.typesafe.slick" %% "slick" % "2.1.0",
"com.novell.ldap" % "jldap" % "2009-10-07",
"org.quartz-scheduler" % "quartz" % "2.2.1",
"com.h2database" % "h2" % "1.4.180",
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
@@ -53,7 +52,7 @@ object MyBuild extends Build {
"com.typesafe.play" %% "twirl-compiler" % "1.0.2"
),
EclipseKeys.withSource := true,
javacOptions in compile ++= Seq("-target", "6", "-source", "6"),
javacOptions in compile ++= Seq("-target", "7", "-source", "7"),
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),
packageOptions += Package.MainClass("JettyLauncher")
).enablePlugins(SbtTwirl)

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import _root_.servlet.{PluginActionInvokeFilter, BasicAuthenticationFilter, TransactionFilter}
import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter}
import app._
//import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
import org.scalatra._
@@ -10,12 +10,11 @@ class ScalatraBootstrap extends LifeCycle {
// Register TransactionFilter and BasicAuthenticationFilter at first
context.addFilter("transactionFilter", new TransactionFilter)
context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
context.addFilter("pluginActionInvokeFilter", new PluginActionInvokeFilter)
context.getFilterRegistration("pluginActionInvokeFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter)
context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
// Register controllers
context.mount(new AnonymousAccessController, "/*")
context.mount(new IndexController, "/")
context.mount(new SearchController, "/")
context.mount(new FileUploadController, "/upload")

View File

@@ -88,6 +88,12 @@ trait AccountControllerBase extends AccountManagementControllerBase {
"name" -> trim(label("Repository name", text(required)))
)(ForkRepositoryForm.apply)
case class AccountForm(accountName: String)
val accountForm = mapping(
"account" -> trim(label("Group/User name", text(required, validAccountName)))
)(AccountForm.apply)
/**
* Displays user information.
*/
@@ -129,8 +135,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
get("/:userName/_avatar"){
val userName = params("userName")
getAccountByUserName(userName).flatMap(_.image).map { image =>
contentType = FileUtil.getMimeType(image)
new java.io.File(getUserUploadDir(userName), image)
RawData(FileUtil.getMimeType(image), new java.io.File(getUserUploadDir(userName), image))
} getOrElse {
contentType = "image/png"
Thread.currentThread.getContextClassLoader.getResourceAsStream("noimage.png")
@@ -285,7 +290,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
* Show the new repository form.
*/
get("/new")(usersOnly {
account.html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
account.html.newrepo(getGroupsByUserName(context.loginAccount.get.userName), context.settings.isCreateRepoOptionPublic)
})
/**
@@ -354,11 +359,31 @@ trait AccountControllerBase extends AccountManagementControllerBase {
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
val groups = getGroupsByUserName(loginUserName)
groups match {
case _: List[String] =>
val managerPermissions = groups.map { group =>
val members = getGroupMembers(group)
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })
}
_root_.helper.html.forkrepository(
repository,
(groups zip managerPermissions).toMap
)
case _ => redirect(s"/${loginUserName}")
}
})
LockUtil.lock(s"${loginUserName}/${repository.name}"){
if(repository.owner == loginUserName || getRepository(loginAccount.userName, repository.name, baseUrl).isDefined){
post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) =>
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
val accountName = form.accountName
LockUtil.lock(s"${accountName}/${repository.name}"){
if(getRepository(accountName, repository.name, baseUrl).isDefined ||
(accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))){
// redirect to the repository if repository already exists
redirect(s"/${loginUserName}/${repository.name}")
redirect(s"/${accountName}/${repository.name}")
} else {
// Insert to the database at first
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
@@ -366,7 +391,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
createRepository(
repositoryName = repository.name,
userName = loginUserName,
userName = accountName,
description = repository.repository.description,
isPrivate = repository.repository.isPrivate,
originRepositoryName = Some(originRepositoryName),
@@ -376,22 +401,22 @@ trait AccountControllerBase extends AccountManagementControllerBase {
)
// Insert default labels
insertDefaultLabels(loginUserName, repository.name)
insertDefaultLabels(accountName, repository.name)
// clone repository actually
JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name),
getRepositoryDir(loginUserName, repository.name))
getRepositoryDir(accountName, repository.name))
// Create Wiki repository
JGitUtil.cloneRepository(
getWikiRepositoryDir(repository.owner, repository.name),
getWikiRepositoryDir(loginUserName, repository.name))
getWikiRepositoryDir(accountName, repository.name))
// Record activity
recordForkActivity(repository.owner, repository.name, loginUserName)
recordForkActivity(repository.owner, repository.name, loginUserName, accountName)
// redirect to the repository
redirect(s"/${loginUserName}/${repository.name}")
redirect(s"/${accountName}/${repository.name}")
}
}
})
@@ -431,4 +456,13 @@ trait AccountControllerBase extends AccountManagementControllerBase {
case None => Some("Key is invalid.")
}
}
private def validAccountName: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = {
getAccountByUserName(value) match {
case Some(_) => None
case None => Some("Invalid Group/User Account.")
}
}
}
}

View File

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

View File

@@ -134,6 +134,18 @@ abstract class ControllerBase extends ScalatraFilter
if (path.startsWith("http")) path
else baseUrl + super.url(path, params, false, false, false)
/**
* Use this method to response the raw data against XSS.
*/
protected def RawData[T](contentType: String, rawData: T): T = {
if(contentType.split(";").head.trim.toLowerCase.startsWith("text/html")){
this.contentType = "text/plain"
} else {
this.contentType = contentType
}
response.addHeader("X-Content-Type-Options", "nosniff")
rawData
}
}
/**

View File

@@ -1,18 +1,33 @@
package app
import service._
import util.{UsersAuthenticator, Keys}
import util.{StringUtil, UsersAuthenticator, Keys}
import util.Implicits._
import service.IssuesService.IssueSearchCondition
class DashboardController extends DashboardControllerBase
with IssuesService with PullRequestService with RepositoryService with AccountService
with UsersAuthenticator
trait DashboardControllerBase extends ControllerBase {
self: IssuesService with PullRequestService with RepositoryService with UsersAuthenticator =>
self: IssuesService with PullRequestService with RepositoryService with AccountService
with UsersAuthenticator =>
get("/dashboard/issues/repos")(usersOnly {
searchIssues("all")
get("/dashboard/issues")(usersOnly {
val q = request.getParameter("q")
val account = context.loginAccount.get
Option(q).map { q =>
val condition = IssueSearchCondition(q, Map[String, Int]())
q match {
case q if(q.contains("is:pr")) => redirect(s"/dashboard/pulls?q=${StringUtil.urlEncode(q)}")
case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/issues/created_by${condition.toURL}")
case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/issues/assigned${condition.toURL}")
case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/issues/mentioned${condition.toURL}")
case _ => searchIssues("created_by")
}
} getOrElse {
searchIssues("created_by")
}
})
get("/dashboard/issues/assigned")(usersOnly {
@@ -23,87 +38,99 @@ trait DashboardControllerBase extends ControllerBase {
searchIssues("created_by")
})
get("/dashboard/issues/mentioned")(usersOnly {
searchIssues("mentioned")
})
get("/dashboard/pulls")(usersOnly {
searchPullRequests("created_by", None)
val q = request.getParameter("q")
val account = context.loginAccount.get
Option(q).map { q =>
val condition = IssueSearchCondition(q, Map[String, Int]())
q match {
case q if(q.contains("is:issue")) => redirect(s"/dashboard/issues?q=${StringUtil.urlEncode(q)}")
case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/pulls/created_by${condition.toURL}")
case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/pulls/assigned${condition.toURL}")
case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/pulls/mentioned${condition.toURL}")
case _ => searchPullRequests("created_by")
}
} getOrElse {
searchPullRequests("created_by")
}
})
get("/dashboard/pulls/owned")(usersOnly {
searchPullRequests("created_by", None)
get("/dashboard/pulls/created_by")(usersOnly {
searchPullRequests("created_by")
})
get("/dashboard/pulls/public")(usersOnly {
searchPullRequests("not_created_by", None)
get("/dashboard/pulls/assigned")(usersOnly {
searchPullRequests("assigned")
})
get("/dashboard/pulls/for/:owner/:repository")(usersOnly {
searchPullRequests("all", Some(params("owner") + "/" + params("repository")))
get("/dashboard/pulls/mentioned")(usersOnly {
searchPullRequests("mentioned")
})
private def getOrCreateCondition(key: String, filter: String, userName: String) = {
val condition = session.putAndGet(key, if(request.hasQueryString){
val q = request.getParameter("q")
if(q == null){
IssueSearchCondition(request)
} else {
IssueSearchCondition(q, Map[String, Int]())
}
} else session.getAs[IssueSearchCondition](key).getOrElse(IssueSearchCondition()))
filter match {
case "assigned" => condition.copy(assigned = Some(userName), author = None , mentioned = None)
case "mentioned" => condition.copy(assigned = None , author = None , mentioned = Some(userName))
case _ => condition.copy(assigned = None , author = Some(userName), mentioned = None)
}
}
private def searchIssues(filter: String) = {
import IssuesService._
// condition
val condition = session.putAndGet(Keys.Session.DashboardIssues,
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition())
)
val userName = context.loginAccount.get.userName
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
//val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request)
val userName = context.loginAccount.get.userName
val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName)
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
val page = IssueSearchCondition.page(request)
dashboard.html.issues(
dashboard.html.issueslist(
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
page,
countIssue(condition.copy(state = "open" ), false, userRepos: _*),
countIssue(condition.copy(state = "closed"), false, userRepos: _*),
condition),
countIssue(condition.copy(assigned = None, author = None), false, userRepos: _*),
countIssue(condition.copy(assigned = Some(userName), author = None), false, userRepos: _*),
countIssue(condition.copy(assigned = None, author = Some(userName)), false, userRepos: _*),
countIssueGroupByRepository(condition, false, userRepos: _*),
condition,
filter)
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
page,
countIssue(condition.copy(state = "open" ), false, userRepos: _*),
countIssue(condition.copy(state = "closed"), false, userRepos: _*),
filter match {
case "assigned" => condition.copy(assigned = Some(userName))
case "mentioned" => condition.copy(mentioned = Some(userName))
case _ => condition.copy(author = Some(userName))
},
filter,
getGroupNames(userName))
}
private def searchPullRequests(filter: String, repository: Option[String]) = {
private def searchPullRequests(filter: String) = {
import IssuesService._
import PullRequestService._
// condition
val condition = session.putAndGet(Keys.Session.DashboardPulls, {
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](Keys.Session.DashboardPulls).getOrElse(IssueSearchCondition())
}.copy(repo = repository))
val userName = context.loginAccount.get.userName
val allRepos = getAllRepositories(userName)
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request)
val counts = countIssueGroupByRepository(
IssueSearchCondition().copy(state = condition.state), true, userRepos: _*)
val userName = context.loginAccount.get.userName
val condition = getOrCreateCondition(Keys.Session.DashboardPulls, filter, userName)
val allRepos = getAllRepositories(userName)
val page = IssueSearchCondition.page(request)
dashboard.html.pulls(
dashboard.html.pullslist(
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
page,
countIssue(condition.copy(state = "open" ), true, allRepos: _*),
countIssue(condition.copy(state = "closed"), true, allRepos: _*),
condition,
None,
false),
getAllPullRequestCountGroupByUser(condition.state == "closed", userName),
userRepos.map { case (userName, repoName) =>
(userName, repoName, counts.find { x => x._1 == userName && x._2 == repoName }.map(_._3).getOrElse(0))
}.sortBy(_._3).reverse,
condition,
filter)
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
page,
countIssue(condition.copy(state = "open" ), true, allRepos: _*),
countIssue(condition.copy(state = "closed"), true, allRepos: _*),
filter match {
case "assigned" => condition.copy(assigned = Some(userName))
case "mentioned" => condition.copy(mentioned = Some(userName))
case _ => condition.copy(author = Some(userName))
},
filter,
getGroupNames(userName))
}

View File

@@ -9,7 +9,6 @@ import util.Implicits._
import util.ControlUtil._
import org.scalatra.Ok
import model.Issue
import plugin.PluginSystem
class IssuesController extends IssuesControllerBase
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
@@ -50,7 +49,12 @@ trait IssuesControllerBase extends ControllerBase {
)(IssueStateForm.apply)
get("/:owner/:repository/issues")(referrersOnly { repository =>
searchIssues(repository)
val q = request.getParameter("q")
if(Option(q).exists(_.contains("is:pr"))){
redirect(s"/${repository.owner}/${repository.name}/pulls?q=" + StringUtil.urlEncode(q))
} else {
searchIssues(repository)
}
})
get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
@@ -195,7 +199,7 @@ trait IssuesControllerBase extends ControllerBase {
org.json4s.jackson.Serialization.write(
Map("title" -> x.title,
"content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
repository, false, true)
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.openedUserName))
))
}
} else Unauthorized
@@ -212,7 +216,7 @@ trait IssuesControllerBase extends ControllerBase {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("content" -> view.Markdown.toHtml(x.content,
repository, false, true)
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName))
))
}
} else Unauthorized
@@ -288,8 +292,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 =>
contentType = FileUtil.getMimeType(file.getName)
file
RawData(FileUtil.getMimeType(file.getName), file)
}
case _ => None
}) getOrElse NotFound
@@ -390,8 +393,14 @@ trait IssuesControllerBase extends ControllerBase {
// retrieve search condition
val condition = session.putAndGet(sessionKey,
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
if(request.hasQueryString){
val q = request.getParameter("q")
if(q == null){
IssueSearchCondition(request)
} else {
IssueSearchCondition(q, getMilestones(owner, repoName).map(x => (x.title, x.milestoneId)).toMap)
}
} else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
issues.html.list(

View File

@@ -25,7 +25,7 @@ trait LabelsControllerBase extends ControllerBase {
get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
issues.labels.html.list(
getLabels(repository.owner, repository.name),
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
@@ -39,7 +39,7 @@ trait LabelsControllerBase extends ControllerBase {
issues.labels.html.label(
getLabel(repository.owner, repository.name, labelId).get,
// TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
@@ -55,7 +55,7 @@ trait LabelsControllerBase extends ControllerBase {
issues.labels.html.label(
getLabel(repository.owner, repository.name, params("labelId").toInt).get,
// TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})

View File

@@ -1,6 +1,6 @@
package app
import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier, Keys}
import util._
import util.Directory._
import util.Implicits._
import util.ControlUtil._
@@ -12,20 +12,21 @@ import scala.collection.JavaConverters._
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
import service.IssuesService._
import service.PullRequestService._
import util.JGitUtil.DiffInfo
import util.JGitUtil.CommitInfo
import org.slf4j.LoggerFactory
import org.eclipse.jgit.merge.MergeStrategy
import org.eclipse.jgit.errors.NoMergeBaseException
import service.WebHookService.WebHookPayload
import util.JGitUtil.DiffInfo
import util.JGitUtil.CommitInfo
class PullRequestsController extends PullRequestsControllerBase
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
with ActivityService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator
with CommitsService with ActivityService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator
trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
with ActivityService with PullRequestService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
with CommitsService with ActivityService with PullRequestService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
@@ -59,7 +60,12 @@ trait PullRequestsControllerBase extends ControllerBase {
case class MergeForm(message: String)
get("/:owner/:repository/pulls")(referrersOnly { repository =>
searchPullRequests(None, repository)
val q = request.getParameter("q")
if(Option(q).exists(_.contains("is:issue"))){
redirect(s"/${repository.owner}/${repository.name}/issues?q=" + StringUtil.urlEncode(q))
} else {
searchPullRequests(None, repository)
}
})
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
@@ -73,7 +79,8 @@ trait PullRequestsControllerBase extends ControllerBase {
pulls.html.pullreq(
issue, pullreq,
getComments(owner, name, issueId),
(commits.flatten.map(commit => getCommitComments(owner, name, commit.id, true)).flatten.toList ::: getComments(owner, name, issueId))
.sortWith((a, b) => a.registeredDate before b.registeredDate),
getIssueLabels(owner, name, issueId),
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestonesWithIssueCount(owner, name),
@@ -273,6 +280,7 @@ trait PullRequestsControllerBase extends ControllerBase {
case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
},
commits.flatten.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)).flatten.toList,
originBranch,
forkedBranch,
oldId.getName,

View File

@@ -94,7 +94,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
}
}
// Change repository HEAD
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
using(Git.open(getRepositoryDir(repository.owner, form.repositoryName))) { git =>
git.getRepository.updateRef(Constants.HEAD, true).link(Constants.R_HEADS + defaultBranch)
}
flash += "info" -> "Repository settings has been updated."

View File

@@ -20,16 +20,16 @@ import org.eclipse.jgit.revwalk.RevCommit
import service.WebHookService.WebHookPayload
class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService
with ReferrerAuthenticator with CollaboratorsAuthenticator
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService
/**
* The repository viewer.
*/
trait RepositoryViewerControllerBase extends ControllerBase {
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService =>
ArchiveCommand.registerFormat("zip", new ZipFormat)
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
@@ -52,6 +52,14 @@ trait RepositoryViewerControllerBase extends ControllerBase {
fileName: String
)
case class CommentForm(
fileName: Option[String],
oldLineNumber: Option[Int],
newLineNumber: Option[Int],
content: String,
issueId: Option[Int]
)
val editorForm = mapping(
"branch" -> trim(label("Branch", text(required))),
"path" -> trim(label("Path", text())),
@@ -70,6 +78,14 @@ trait RepositoryViewerControllerBase extends ControllerBase {
"fileName" -> trim(label("Filename", text(required)))
)(DeleteForm.apply)
val commentForm = mapping(
"fileName" -> trim(label("Filename", optional(text()))),
"oldLineNumber" -> trim(label("Old line number", optional(number()))),
"newLineNumber" -> trim(label("New line number", optional(number()))),
"content" -> trim(label("Content", text(required))),
"issueId" -> trim(label("Issue Id", optional(number())))
)(CommentForm.apply)
/**
* Returns converted HTML from Markdown for preview.
*/
@@ -77,7 +93,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
contentType = "text/html"
view.helpers.markdown(params("content"), repository,
params("enableWikiLink").toBoolean,
params("enableRefsLink").toBoolean)
params("enableRefsLink").toBoolean,
params("enableTaskList").toBoolean,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
/**
@@ -112,7 +130,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
}, page, hasNext)
}, page, hasNext, hasWritePermission(repository.owner, repository.name, context.loginAccount))
case Left(_) => NotFound
}
}
@@ -196,8 +214,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
if(raw){
// Download
defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes =>
contentType = FileUtil.getContentType(path, bytes)
bytes
RawData(FileUtil.getContentType(path, bytes), bytes)
}
} else {
repo.html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId),
@@ -219,12 +236,88 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
JGitUtil.getTagsOfCommit(git, revCommit.getName),
repository, diffs, oldCommitId)
getCommitComments(repository.owner, repository.name, id, false),
repository, diffs, oldCommitId, hasWritePermission(repository.owner, repository.name, context.loginAccount))
}
}
}
})
post("/:owner/:repository/commit/:id/comment/new", commentForm)(readableUsersOnly { (form, repository) =>
val id = params("id")
createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName, form.content,
form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId.isDefined)
form.issueId match {
case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
}
redirect(s"/${repository.owner}/${repository.name}/commit/${id}")
})
ajaxGet("/:owner/:repository/commit/:id/comment/_form")(readableUsersOnly { repository =>
val id = params("id")
val fileName = params.get("fileName")
val oldLineNumber = params.get("oldLineNumber") map (_.toInt)
val newLineNumber = params.get("newLineNumber") map (_.toInt)
val issueId = params.get("issueId") map (_.toInt)
repo.html.commentform(
commitId = id,
fileName, oldLineNumber, newLineNumber, issueId,
hasWritePermission = hasWritePermission(repository.owner, repository.name, context.loginAccount),
repository = repository
)
})
ajaxPost("/:owner/:repository/commit/:id/comment/_data/new", commentForm)(readableUsersOnly { (form, repository) =>
val id = params("id")
val commentId = createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName,
form.content, form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId.isDefined)
form.issueId match {
case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
}
helper.html.commitcomment(getCommitComment(repository.owner, repository.name, commentId.toString).get,
hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
})
ajaxGet("/:owner/:repository/commit_comments/_data/:id")(readableUsersOnly { repository =>
getCommitComment(repository.owner, repository.name, params("id")) map { x =>
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
params.get("dataType") collect {
case t if t == "html" => repo.html.editcomment(
x.content, x.commentId, x.userName, x.repositoryName)
} getOrElse {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("content" -> view.Markdown.toHtml(x.content,
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName))
))
}
} else Unauthorized
} getOrElse NotFound
})
ajaxPost("/:owner/:repository/commit_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
defining(repository.owner, repository.name){ case (owner, name) =>
getCommitComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){
updateCommitComment(comment.commentId, form.content)
redirect(s"/${owner}/${name}/commit_comments/_data/${comment.commentId}")
} else Unauthorized
} getOrElse NotFound
}
})
ajaxPost("/:owner/:repository/commit_comments/delete/:id")(readableUsersOnly { repository =>
defining(repository.owner, repository.name){ case (owner, name) =>
getCommitComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){
Ok(deleteCommitComment(comment.commentId))
} else Unauthorized
} getOrElse NotFound
}
})
/**
* Displays branches.
*/
@@ -239,6 +332,24 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
})
/**
* Creates a branch.
*/
post("/:owner/:repository/branches")(collaboratorsOnly { repository =>
val newBranchName = params.getOrElse("new", halt(400))
val fromBranchName = params.getOrElse("from", halt(400))
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.createBranch(git, fromBranchName, newBranchName)
} match {
case Right(message) =>
flash += "info" -> message
redirect(s"/${repository.owner}/${repository.name}/tree/${StringUtil.urlEncode(newBranchName).replace("%2F", "/")}")
case Left(message) =>
flash += "error" -> message
redirect(s"/${repository.owner}/${repository.name}/tree/${fromBranchName}")
}
})
/**
* Deletes branch.
*/
@@ -330,8 +441,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repo.html.files(revision, repository,
if(path == ".") Nil else path.split("/").toList, // current path
context.loginAccount match {
case None => List()
case account: Option[model.Account] => getGroupsByUserName(account.get.userName)
}, // groups of current user
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount))
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount),
flash.get("info"), flash.get("error"))
}
} getOrElse NotFound
}
@@ -379,6 +495,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
//refUpdate.setRefLogMessage("merged", true)
refUpdate.update()
// update pull request
updatePullRequests(repository.owner, repository.name, branch)
// record activity
recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch,
List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))))
@@ -415,7 +534,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
}
private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): File = {
private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): Unit = {
val revision = name.stripSuffix(suffix)
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
if(workDir.exists) {
@@ -423,21 +542,26 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
workDir.mkdirs
val file = new File(workDir, repository.name + "-" +
(if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + suffix)
val filename = repository.name + "-" +
(if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + suffix
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
using(new java.io.FileOutputStream(file)) { out =>
git.archive
.setFormat(suffix.tail)
.setTree(revCommit.getTree)
.setOutputStream(out)
.call()
}
contentType = "application/octet-stream"
response.setHeader("Content-Disposition", s"attachment; filename=${file.getName}")
file
response.setHeader("Content-Disposition", s"attachment; filename=${filename}")
response.setBufferSize(1024 * 1024);
git.archive
.setFormat(suffix.tail)
.setTree(revCommit.getTree)
.setOutputStream(response.getOutputStream)
.call()
Unit
}
}
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
}

View File

@@ -3,15 +3,8 @@ package app
import service.{AccountService, SystemSettingsService}
import SystemSettingsService._
import util.AdminAuthenticator
import util.Directory._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._
import ssh.SshServer
import org.apache.commons.io.FileUtils
import java.io.FileInputStream
import plugin.{Plugin, PluginSystem}
import org.scalatra.Ok
import util.Implicits._
class SystemSettingsController extends SystemSettingsControllerBase
with AccountService with AdminAuthenticator
@@ -21,7 +14,10 @@ trait SystemSettingsControllerBase extends ControllerBase {
private val form = mapping(
"baseUrl" -> trim(label("Base URL", optional(text()))),
"information" -> trim(label("Information", optional(text()))),
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
"allowAnonymousAccess" -> trim(label("Anonymous access", boolean())),
"isCreateRepoOptionPublic" -> trim(label("Default option to create a new repository", boolean())),
"gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())),
"ssh" -> trim(label("SSH access", boolean())),
@@ -47,6 +43,7 @@ trait SystemSettingsControllerBase extends ControllerBase {
"fullNameAttribute" -> trim(label("Full name attribute", optional(text()))),
"mailAttribute" -> trim(label("Mail address attribute", optional(text()))),
"tls" -> trim(label("Enable TLS", optional(boolean()))),
"ssl" -> trim(label("Enable SSL", optional(boolean()))),
"keystore" -> trim(label("Keystore", optional(text())))
)(Ldap.apply))
)(SystemSettings.apply).verifying { settings =>
@@ -84,118 +81,4 @@ trait SystemSettingsControllerBase extends ControllerBase {
redirect("/admin/system")
})
get("/admin/plugins")(adminOnly {
if(enablePluginSystem){
val installedPlugins = plugin.PluginSystem.plugins
val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable")
admin.plugins.html.installed(installedPlugins, updatablePlugins)
} else NotFound
})
post("/admin/plugins/_update", pluginForm)(adminOnly { form =>
if(enablePluginSystem){
deletePlugins(form.pluginIds)
installPlugins(form.pluginIds)
redirect("/admin/plugins")
} else NotFound
})
post("/admin/plugins/_delete", pluginForm)(adminOnly { form =>
if(enablePluginSystem){
deletePlugins(form.pluginIds)
redirect("/admin/plugins")
} else NotFound
})
get("/admin/plugins/available")(adminOnly {
if(enablePluginSystem){
val installedPlugins = plugin.PluginSystem.plugins
val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available")
admin.plugins.html.available(availablePlugins)
} else NotFound
})
post("/admin/plugins/_install", pluginForm)(adminOnly { form =>
if(enablePluginSystem){
installPlugins(form.pluginIds)
redirect("/admin/plugins")
} else NotFound
})
get("/admin/plugins/console")(adminOnly {
if(enablePluginSystem){
admin.plugins.html.console()
} else NotFound
})
post("/admin/plugins/console")(adminOnly {
if(enablePluginSystem){
val script = request.getParameter("script")
val result = plugin.ScalaPlugin.eval(script)
Ok()
} else NotFound
})
// TODO Move these methods to PluginSystem or Service?
private def deletePlugins(pluginIds: List[String]): Unit = {
pluginIds.foreach { pluginId =>
plugin.PluginSystem.uninstall(pluginId)
val dir = new java.io.File(PluginHome, pluginId)
if(dir.exists && dir.isDirectory){
FileUtils.deleteQuietly(dir)
PluginSystem.uninstall(pluginId)
}
}
}
private def installPlugins(pluginIds: List[String]): Unit = {
val dir = getPluginCacheDir()
val installedPlugins = plugin.PluginSystem.plugins
getAvailablePlugins(installedPlugins).filter(x => pluginIds.contains(x.id)).foreach { plugin =>
val pluginDir = new java.io.File(PluginHome, plugin.id)
if(pluginDir.exists){
FileUtils.deleteDirectory(pluginDir)
}
FileUtils.copyDirectory(new java.io.File(dir, plugin.repository + "/" + plugin.id), pluginDir)
PluginSystem.installPlugin(plugin.id)
}
}
private def getAvailablePlugins(installedPlugins: List[Plugin]): List[SystemSettingsControllerBase.AvailablePlugin] = {
val repositoryRoot = getPluginCacheDir()
if(repositoryRoot.exists && repositoryRoot.isDirectory){
PluginSystem.repositories.flatMap { repo =>
val repoDir = new java.io.File(repositoryRoot, repo.id)
if(repoDir.exists && repoDir.isDirectory){
repoDir.listFiles.filter(d => d.isDirectory && !d.getName.startsWith(".")).map { plugin =>
val propertyFile = new java.io.File(plugin, "plugin.properties")
val properties = new java.util.Properties()
if(propertyFile.exists && propertyFile.isFile){
using(new FileInputStream(propertyFile)){ in =>
properties.load(in)
}
}
SystemSettingsControllerBase.AvailablePlugin(
repository = repo.id,
id = properties.getProperty("id"),
version = properties.getProperty("version"),
author = properties.getProperty("author"),
url = properties.getProperty("url"),
description = properties.getProperty("description"),
status = installedPlugins.find(_.id == properties.getProperty("id")) match {
case Some(x) if(PluginSystem.isUpdatable(x.version, properties.getProperty("version")))=> "updatable"
case Some(x) => "installed"
case None => "available"
})
}
} else Nil
}
} else Nil
}
}
object SystemSettingsControllerBase {
case class AvailablePlugin(repository: String, id: String, version: String,
author: String, url: String, description: String, status: String)
}

View File

@@ -49,7 +49,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"clearImage" -> trim(label("Clear image" ,boolean())),
"removed" -> trim(label("Disable" ,boolean()))
"removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName"))))
)(EditUserForm.apply)
val newGroupForm = mapping(
@@ -190,4 +190,14 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
}
}
protected def disableByNotYourself(paramName: String): Constraint = new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] = {
params.get(paramName).flatMap { userName =>
if(userName == context.loginAccount.get.userName)
Some("You can't disable your account yourself")
else
None
}
}
}
}

View File

@@ -164,8 +164,7 @@ trait WikiControllerBase extends ControllerBase {
val path = multiParams("splat").head
getFileContent(repository.owner, repository.name, path).map { bytes =>
contentType = FileUtil.getContentType(path, bytes)
bytes
RawData(FileUtil.getContentType(path, bytes), bytes)
} getOrElse NotFound
})

View File

@@ -44,4 +44,11 @@ protected[model] trait TemplateComponent { self: Profile =>
byRepository(userName, repositoryName) && (this.milestoneId === milestoneId)
}
trait CommitTemplate extends BasicTemplate { self: Table[_] =>
val commitId = column[String]("COMMIT_ID")
def byCommit(owner: String, repository: String, commitId: String) =
byRepository(owner, repository) && (this.commitId === commitId)
}
}

View File

@@ -0,0 +1,78 @@
package model
trait Comment {
val commentedUserName: String
val registeredDate: java.util.Date
}
trait IssueCommentComponent extends TemplateComponent { self: Profile =>
import profile.simple._
import self._
lazy val IssueComments = new TableQuery(tag => new IssueComments(tag)){
def autoInc = this returning this.map(_.commentId)
}
class IssueComments(tag: Tag) extends Table[IssueComment](tag, "ISSUE_COMMENT") with IssueTemplate {
val commentId = column[Int]("COMMENT_ID", O AutoInc)
val action = column[String]("ACTION")
val commentedUserName = column[String]("COMMENTED_USER_NAME")
val content = column[String]("CONTENT")
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
val updatedDate = column[java.util.Date]("UPDATED_DATE")
def * = (userName, repositoryName, issueId, commentId, action, commentedUserName, content, registeredDate, updatedDate) <> (IssueComment.tupled, IssueComment.unapply)
def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind
}
}
case class IssueComment (
userName: String,
repositoryName: String,
issueId: Int,
commentId: Int = 0,
action: String,
commentedUserName: String,
content: String,
registeredDate: java.util.Date,
updatedDate: java.util.Date
) extends Comment
trait CommitCommentComponent extends TemplateComponent { self: Profile =>
import profile.simple._
import self._
lazy val CommitComments = new TableQuery(tag => new CommitComments(tag)){
def autoInc = this returning this.map(_.commentId)
}
class CommitComments(tag: Tag) extends Table[CommitComment](tag, "COMMIT_COMMENT") with CommitTemplate {
val commentId = column[Int]("COMMENT_ID", O AutoInc)
val commentedUserName = column[String]("COMMENTED_USER_NAME")
val content = column[String]("CONTENT")
val fileName = column[Option[String]]("FILE_NAME")
val oldLine = column[Option[Int]]("OLD_LINE_NUMBER")
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)
def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind
}
}
case class CommitComment(
userName: String,
repositoryName: String,
commitId: String,
commentId: Int = 0,
commentedUserName: String,
content: String,
fileName: Option[String],
oldLine: Option[Int],
newLine: Option[Int],
registeredDate: java.util.Date,
updatedDate: java.util.Date,
pullRequest: Boolean
) extends Comment

View File

@@ -1,34 +0,0 @@
package model
trait IssueCommentComponent extends TemplateComponent { self: Profile =>
import profile.simple._
import self._
lazy val IssueComments = new TableQuery(tag => new IssueComments(tag)){
def autoInc = this returning this.map(_.commentId)
}
class IssueComments(tag: Tag) extends Table[IssueComment](tag, "ISSUE_COMMENT") with IssueTemplate {
val commentId = column[Int]("COMMENT_ID", O AutoInc)
val action = column[String]("ACTION")
val commentedUserName = column[String]("COMMENTED_USER_NAME")
val content = column[String]("CONTENT")
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
val updatedDate = column[java.util.Date]("UPDATED_DATE")
def * = (userName, repositoryName, issueId, commentId, action, commentedUserName, content, registeredDate, updatedDate) <> (IssueComment.tupled, IssueComment.unapply)
def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind
}
}
case class IssueComment(
userName: String,
repositoryName: String,
issueId: Int,
commentId: Int = 0,
action: String,
commentedUserName: String,
content: String,
registeredDate: java.util.Date,
updatedDate: java.util.Date
)

View File

@@ -22,6 +22,7 @@ object Profile extends {
} with AccountComponent
with ActivityComponent
with CollaboratorComponent
with CommitCommentComponent
with GroupMemberComponent
with IssueComponent
with IssueCommentComponent

View File

@@ -1,22 +0,0 @@
package plugin
import plugin.PluginSystem._
import java.sql.Connection
trait Plugin {
val id: String
val version: String
val author: String
val url: String
val description: String
def repositoryMenus : List[RepositoryMenu]
def globalMenus : List[GlobalMenu]
def repositoryActions : List[RepositoryAction]
def globalActions : List[Action]
def javaScripts : List[JavaScript]
}
object PluginConnectionHolder {
val threadLocal = new ThreadLocal[Connection]
}

View File

@@ -1,194 +0,0 @@
package plugin
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import org.slf4j.LoggerFactory
import java.util.concurrent.atomic.AtomicBoolean
import util.Directory._
import util.ControlUtil._
import org.apache.commons.io.{IOUtils, FileUtils}
import Security._
import service.PluginService
import model.Profile._
import profile.simple._
import java.io.FileInputStream
import java.sql.Connection
import app.Context
import service.RepositoryService.RepositoryInfo
/**
* Provides extension points to plug-ins.
*/
object PluginSystem extends PluginService {
private val logger = LoggerFactory.getLogger(PluginSystem.getClass)
private val initialized = new AtomicBoolean(false)
private val pluginsMap = scala.collection.mutable.Map[String, Plugin]()
private val repositoriesList = scala.collection.mutable.ListBuffer[PluginRepository]()
def install(plugin: Plugin): Unit = {
pluginsMap.put(plugin.id, plugin)
}
def plugins: List[Plugin] = pluginsMap.values.toList
def uninstall(id: String)(implicit session: Session): Unit = {
pluginsMap.remove(id)
// Delete from PLUGIN table
deletePlugin(id)
// Drop tables
val pluginDir = new java.io.File(PluginHome)
val sqlFile = new java.io.File(pluginDir, s"${id}/sql/drop.sql")
if(sqlFile.exists){
val sql = IOUtils.toString(new FileInputStream(sqlFile), "UTF-8")
using(session.conn.createStatement()){ stmt =>
stmt.executeUpdate(sql)
}
}
}
def repositories: List[PluginRepository] = repositoriesList.toList
/**
* Initializes the plugin system. Load scripts from GITBUCKET_HOME/plugins.
*/
def init()(implicit session: Session): Unit = {
if(initialized.compareAndSet(false, true)){
// Load installed plugins
val pluginDir = new java.io.File(PluginHome)
if(pluginDir.exists && pluginDir.isDirectory){
pluginDir.listFiles.filter(f => f.isDirectory && !f.getName.startsWith(".")).foreach { dir =>
installPlugin(dir.getName)
}
}
// Add default plugin repositories
repositoriesList += PluginRepository("central", "https://github.com/takezoe/gitbucket_plugins.git")
}
}
// TODO Method name seems to not so good.
def installPlugin(id: String)(implicit session: Session): Unit = {
val pluginHome = new java.io.File(PluginHome)
val pluginDir = new java.io.File(pluginHome, id)
val scalaFile = new java.io.File(pluginDir, "plugin.scala")
if(scalaFile.exists && scalaFile.isFile){
val properties = new java.util.Properties()
using(new java.io.FileInputStream(new java.io.File(pluginDir, "plugin.properties"))){ in =>
properties.load(in)
}
val pluginId = properties.getProperty("id")
val version = properties.getProperty("version")
val author = properties.getProperty("author")
val url = properties.getProperty("url")
val description = properties.getProperty("description")
val source = s"""
|val id = "${pluginId}"
|val version = "${version}"
|val author = "${author}"
|val url = "${url}"
|val description = "${description}"
""".stripMargin + FileUtils.readFileToString(scalaFile, "UTF-8")
try {
// Compile and eval Scala source code
ScalaPlugin.eval(pluginDir.listFiles.filter(_.getName.endsWith(".scala.html")).map { file =>
ScalaPlugin.compileTemplate(
id.replaceAll("-", ""),
file.getName.replaceAll("\\.scala\\.html$", ""),
IOUtils.toString(new FileInputStream(file)))
}.mkString("\n") + source)
// Migrate database
val plugin = getPlugin(pluginId)
if(plugin.isEmpty){
registerPlugin(model.Plugin(pluginId, version))
migrate(session.conn, pluginId, "0.0")
} else {
updatePlugin(model.Plugin(pluginId, version))
migrate(session.conn, pluginId, plugin.get.version)
}
} catch {
case e: Throwable => logger.warn(s"Error in plugin loading for ${scalaFile.getAbsolutePath}", e)
}
}
}
// TODO Should PluginSystem provide a way to migrate resources other than H2?
private def migrate(conn: Connection, pluginId: String, current: String): Unit = {
val pluginDir = new java.io.File(PluginHome)
// TODO Is ot possible to use this migration system in GitBucket migration?
val dim = current.split("\\.")
val currentVersion = Version(dim(0).toInt, dim(1).toInt)
val sqlDir = new java.io.File(pluginDir, s"${pluginId}/sql")
if(sqlDir.exists && sqlDir.isDirectory){
sqlDir.listFiles.filter(_.getName.endsWith(".sql")).map { file =>
val array = file.getName.replaceFirst("\\.sql", "").split("_")
Version(array(0).toInt, array(1).toInt)
}
.sorted.reverse.takeWhile(_ > currentVersion)
.reverse.foreach { version =>
val sqlFile = new java.io.File(pluginDir, s"${pluginId}/sql/${version.major}_${version.minor}.sql")
val sql = IOUtils.toString(new FileInputStream(sqlFile), "UTF-8")
using(conn.createStatement()){ stmt =>
stmt.executeUpdate(sql)
}
}
}
}
case class Version(major: Int, minor: Int) extends Ordered[Version] {
override def compare(that: Version): Int = {
if(major != that.major){
major.compare(that.major)
} else{
minor.compare(that.minor)
}
}
def displayString: String = major + "." + minor
}
def repositoryMenus : List[RepositoryMenu] = pluginsMap.values.flatMap(_.repositoryMenus).toList
def globalMenus : List[GlobalMenu] = pluginsMap.values.flatMap(_.globalMenus).toList
def repositoryActions : List[RepositoryAction] = pluginsMap.values.flatMap(_.repositoryActions).toList
def globalActions : List[Action] = pluginsMap.values.flatMap(_.globalActions).toList
def javaScripts : List[JavaScript] = pluginsMap.values.flatMap(_.javaScripts).toList
// Case classes to hold plug-ins information internally in GitBucket
case class PluginRepository(id: String, url: String)
case class GlobalMenu(label: String, url: String, icon: String, condition: Context => Boolean)
case class RepositoryMenu(label: String, name: String, url: String, icon: String, condition: Context => Boolean)
case class Action(method: String, path: String, security: Security, function: (HttpServletRequest, HttpServletResponse, Context) => Any)
case class RepositoryAction(method: String, path: String, security: Security, function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any)
case class Button(label: String, href: String)
case class JavaScript(filter: String => Boolean, script: String)
/**
* Checks whether the plugin is updatable.
*/
def isUpdatable(oldVersion: String, newVersion: String): Boolean = {
if(oldVersion == newVersion){
false
} else {
val dim1 = oldVersion.split("\\.").map(_.toInt)
val dim2 = newVersion.split("\\.").map(_.toInt)
dim1.zip(dim2).foreach { case (a, b) =>
if(a < b){
return true
} else if(a > b){
return false
}
}
return false
}
}
}

View File

@@ -1,66 +0,0 @@
package plugin
import util.Directory._
import org.eclipse.jgit.api.Git
import org.slf4j.LoggerFactory
import org.quartz.{Scheduler, JobExecutionContext, Job}
import org.quartz.JobBuilder._
import org.quartz.TriggerBuilder._
import org.quartz.SimpleScheduleBuilder._
class PluginUpdateJob extends Job {
private val logger = LoggerFactory.getLogger(classOf[PluginUpdateJob])
private var failedCount = 0
/**
* Clone or pull all plugin repositories
*
* TODO Support plugin repository access through the proxy server
*/
override def execute(context: JobExecutionContext): Unit = {
try {
if(failedCount > 3){
logger.error("Skip plugin information updating because failed count is over limit")
} else {
logger.info("Start plugin information updating")
PluginSystem.repositories.foreach { repository =>
logger.info(s"Updating ${repository.id}: ${repository.url}...")
val dir = getPluginCacheDir()
val repo = new java.io.File(dir, repository.id)
if(repo.exists){
// pull if the repository is already cloned
Git.open(repo).pull().call()
} else {
// clone if the repository is not exist
Git.cloneRepository().setURI(repository.url).setDirectory(repo).call()
}
}
logger.info("End plugin information updating")
}
} catch {
case e: Exception => {
failedCount = failedCount + 1
logger.error("Failed to update plugin information", e)
}
}
}
}
object PluginUpdateJob {
def schedule(scheduler: Scheduler): Unit = {
val job = newJob(classOf[PluginUpdateJob])
.withIdentity("pluginUpdateJob")
.build()
val trigger = newTrigger()
.withIdentity("pluginUpdateTrigger")
.startNow()
.withSchedule(simpleSchedule().withIntervalInHours(24).repeatForever())
.build()
scheduler.scheduleJob(job, trigger)
}
}

View File

@@ -1,77 +0,0 @@
package plugin
import scala.collection.mutable.ListBuffer
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import app.Context
import plugin.PluginSystem._
import plugin.PluginSystem.RepositoryMenu
import plugin.Security._
import service.RepositoryService.RepositoryInfo
import scala.reflect.runtime.currentMirror
import scala.tools.reflect.ToolBox
import play.twirl.compiler.TwirlCompiler
import scala.io.Codec
// TODO This is a sample implementation for Scala based plug-ins.
class ScalaPlugin(val id: String, val version: String,
val author: String, val url: String, val description: String) extends Plugin {
private val repositoryMenuList = ListBuffer[RepositoryMenu]()
private val globalMenuList = ListBuffer[GlobalMenu]()
private val repositoryActionList = ListBuffer[RepositoryAction]()
private val globalActionList = ListBuffer[Action]()
private val javaScriptList = ListBuffer[JavaScript]()
def repositoryMenus : List[RepositoryMenu] = repositoryMenuList.toList
def globalMenus : List[GlobalMenu] = globalMenuList.toList
def repositoryActions : List[RepositoryAction] = repositoryActionList.toList
def globalActions : List[Action] = globalActionList.toList
def javaScripts : List[JavaScript] = javaScriptList.toList
def addRepositoryMenu(label: String, name: String, url: String, icon: String)(condition: (Context) => Boolean): Unit = {
repositoryMenuList += RepositoryMenu(label, name, url, icon, condition)
}
def addGlobalMenu(label: String, url: String, icon: String)(condition: (Context) => Boolean): Unit = {
globalMenuList += GlobalMenu(label, url, icon, condition)
}
def addGlobalAction(method: String, path: String, security: Security = All())(function: (HttpServletRequest, HttpServletResponse, Context) => Any): Unit = {
globalActionList += Action(method, path, security, function)
}
def addRepositoryAction(method: String, path: String, security: Security = All())(function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any): Unit = {
repositoryActionList += RepositoryAction(method, path, security, function)
}
def addJavaScript(filter: String => Boolean, script: String): Unit = {
javaScriptList += JavaScript(filter, script)
}
}
object ScalaPlugin {
def define(id: String, version: String, author: String, url: String, description: String)
= new ScalaPlugin(id, version, author, url, description)
def eval(source: String): Any = {
val toolbox = currentMirror.mkToolBox()
val tree = toolbox.parse(source)
toolbox.eval(tree)
}
def compileTemplate(packageName: String, name: String, source: String): String = {
val result = TwirlCompiler.parseAndGenerateCodeNewParser(
Array(packageName, name),
source.getBytes("UTF-8"),
Codec(scala.util.Properties.sourceEncoding),
"",
"play.twirl.api.HtmlFormat.Appendable",
"play.twirl.api.HtmlFormat",
"",
false)
result.replaceFirst("package .*", "")
}
}

View File

@@ -1,36 +0,0 @@
package plugin
/**
* Defines enum case classes to specify permission for actions which is provided by plugin.
*/
object Security {
sealed trait Security
/**
* All users and guests
*/
case class All() extends Security
/**
* Only signed-in users
*/
case class Login() extends Security
/**
* Only repository owner and collaborators
*/
case class Member() extends Security
/**
* Only repository owner and managers of group repository
*/
case class Owner() extends Security
/**
* Only administrators
*/
case class Admin() extends Security
}

View File

@@ -1,56 +0,0 @@
import java.sql.PreparedStatement
import play.twirl.api.Html
import util.ControlUtil._
import scala.collection.mutable.ListBuffer
package object plugin {
case class Redirect(path: String)
case class Fragment(html: Html)
case class RawData(contentType: String, content: Array[Byte])
object db {
// TODO labelled place holder support
def select(sql: String, params: Any*): Seq[Map[String, String]] = {
defining(PluginConnectionHolder.threadLocal.get){ conn =>
using(conn.prepareStatement(sql)){ stmt =>
setParams(stmt, params: _*)
using(stmt.executeQuery()){ rs =>
val list = new ListBuffer[Map[String, String]]()
while(rs.next){
defining(rs.getMetaData){ meta =>
val map = Range(1, meta.getColumnCount + 1).map { i =>
val name = meta.getColumnName(i)
(name, rs.getString(name))
}.toMap
list += map
}
}
list
}
}
}
}
// TODO labelled place holder support
def update(sql: String, params: Any*): Int = {
defining(PluginConnectionHolder.threadLocal.get){ conn =>
using(conn.prepareStatement(sql)){ stmt =>
setParams(stmt, params: _*)
stmt.executeUpdate()
}
}
}
private def setParams(stmt: PreparedStatement, params: Any*): Unit = {
params.zipWithIndex.foreach { case (p, i) =>
p match {
case x: String => stmt.setString(i + 1, x)
case x: Int => stmt.setInt(i + 1, x)
case x: Boolean => stmt.setBoolean(i + 1, x)
}
}
}
}
}

View File

@@ -168,6 +168,11 @@ trait AccountService {
Repositories.filter(_.userName === userName.bind).delete
}
def getGroupNames(userName: String)(implicit s: Session): List[String] = {
List(userName) ++
Collaborators.filter(_.collaboratorName === userName.bind).sortBy(_.userName).map(_.userName).list
}
}
object AccountService extends AccountService

View File

@@ -95,6 +95,15 @@ trait ActivityService {
Some(cut(comment, 200)),
currentDate)
def recordCommentCommitActivity(userName: String, repositoryName: String, activityUserName: String, commitId: String, comment: String)
(implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"comment_commit",
s"[user:${activityUserName}] commented on commit [commit:${userName}/${repositoryName}@${commitId}]",
Some(cut(comment, 200)),
currentDate
)
def recordCreateWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String)
(implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
@@ -151,10 +160,10 @@ trait ActivityService {
None,
currentDate)
def recordForkActivity(userName: String, repositoryName: String, activityUserName: String)(implicit s: Session): Unit =
def recordForkActivity(userName: String, repositoryName: String, activityUserName: String, forkedUserName: String)(implicit s: Session): Unit =
Activities insert Activity(userName, repositoryName, activityUserName,
"fork",
s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${activityUserName}/${repositoryName}]",
s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${forkedUserName}/${repositoryName}]",
None,
currentDate)

View File

@@ -0,0 +1,52 @@
package service
import scala.slick.jdbc.{StaticQuery => Q}
import Q.interpolation
import model.Profile._
import profile.simple._
import model.CommitComment
import util.Implicits._
import util.StringUtil._
trait CommitsService {
def getCommitComments(owner: String, repository: String, commitId: String, pullRequest: Boolean)(implicit s: Session) =
CommitComments filter {
t => t.byCommit(owner, repository, commitId) && (t.pullRequest === pullRequest || pullRequest)
} list
def getCommitComment(owner: String, repository: String, commentId: String)(implicit s: Session) =
if (commentId forall (_.isDigit))
CommitComments filter { t =>
t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository)
} firstOption
else
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 =
CommitComments.autoInc insert CommitComment(
userName = owner,
repositoryName = repository,
commitId = commitId,
commentedUserName = loginUser,
content = content,
fileName = fileName,
oldLine = oldLine,
newLine = newLine,
registeredDate = currentDate,
updatedDate = currentDate,
pullRequest = pullRequest)
def updateCommitComment(commentId: Int, content: String)(implicit s: Session) =
CommitComments
.filter (_.byPrimaryKey(commentId))
.map { t =>
t.content -> t.updatedDate
}.update (content, currentDate)
def deleteCommitComment(commentId: Int)(implicit s: Session) =
CommitComments filter (_.byPrimaryKey(commentId)) delete
}

View File

@@ -47,8 +47,8 @@ trait IssuesService {
* @param repos Tuple of the repository owner and the repository name
* @return the count of the search result
*/
def countIssue(condition: IssueSearchCondition, onlyPullRequest: Boolean, repos: (String, String)*)
(implicit s: Session): Int =
def countIssue(condition: IssueSearchCondition, onlyPullRequest: Boolean,
repos: (String, String)*)(implicit s: Session): Int =
Query(searchIssueQuery(repos, condition, onlyPullRequest).length).first
/**
@@ -59,8 +59,8 @@ trait IssuesService {
* @param condition the search condition
* @return the Map which contains issue count for each labels (key is label name, value is issue count)
*/
def countIssueGroupByLabels(owner: String, repository: String,
condition: IssueSearchCondition)(implicit s: Session): Map[String, Int] = {
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
filterUser: Map[String, String])(implicit s: Session): Map[String, Int] = {
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), false)
.innerJoin(IssueLabels).on { (t1, t2) =>
@@ -77,28 +77,6 @@ trait IssuesService {
}
.toMap
}
/**
* Returns list which contains issue count for each repository.
* If the issue does not exist, its repository is not included in the result.
*
* @param condition the search condition
* @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
* @param repos Tuple of the repository owner and the repository name
* @return list which contains issue count for each repository
*/
def countIssueGroupByRepository(
condition: IssueSearchCondition, onlyPullRequest: Boolean,
repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = {
searchIssueQuery(repos, condition.copy(repo = None), onlyPullRequest)
.groupBy { t =>
t.userName -> t.repositoryName
}
.map { case (repo, t) =>
(repo._1, repo._2, t.length)
}
.sortBy(_._3 desc)
.list
}
/**
* Returns the search result against issues.
@@ -110,8 +88,7 @@ trait IssuesService {
* @param repos Tuple of the repository owner and the repository name
* @return the search result (list of tuples which contain issue, labels and comment count)
*/
def searchIssue(condition: IssueSearchCondition, pullRequest: Boolean,
offset: Int, limit: Int, repos: (String, String)*)
def searchIssue(condition: IssueSearchCondition, pullRequest: Boolean, offset: Int, limit: Int, repos: (String, String)*)
(implicit s: Session): List[IssueInfo] = {
// get issues and comment count and labels
@@ -156,20 +133,18 @@ trait IssuesService {
/**
* Assembles query for conditional issue searching.
*/
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition,
pullRequest: Boolean)(implicit s: Session) =
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, pullRequest: Boolean)(implicit s: Session) =
Issues filter { t1 =>
condition.repo
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
.getOrElse (repos)
.map { case (owner, repository) => t1.byRepository(owner, repository) }
.foldLeft[Column[Boolean]](false) ( _ || _ ) &&
repos
.map { case (owner, repository) => t1.byRepository(owner, repository) }
.foldLeft[Column[Boolean]](false) ( _ || _ ) &&
(t1.closed === (condition.state == "closed").bind) &&
(t1.milestoneId === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
(t1.milestoneId.? isEmpty, condition.milestoneId == Some(None)) &&
(t1.assignedUserName === condition.assigned.get.bind, condition.assigned.isDefined) &&
(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
(t1.pullRequest === pullRequest.bind) &&
// Label filter
(IssueLabels filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
(t2.labelId in
@@ -177,7 +152,19 @@ trait IssuesService {
(t3.byRepository(t1.userName, t1.repositoryName)) &&
(t3.labelName inSetBind condition.labels)
} map(_.labelId)))
} exists, condition.labels.nonEmpty)
} exists, condition.labels.nonEmpty) &&
// Visibility filter
(Repositories filter { t2 =>
(t2.byRepository(t1.userName, t1.repositoryName)) &&
(t2.isPrivate === (condition.visibility == Some("private")).bind)
} exists, condition.visibility.nonEmpty) &&
// Organization (group) filter
(t1.userName inSetBind condition.groups, condition.groups.nonEmpty) &&
// Mentioned filter
((t1.openedUserName === condition.mentioned.get.bind) || t1.assignedUserName === condition.mentioned.get.bind ||
(IssueComments filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === condition.mentioned.get.bind)
} exists), condition.mentioned.isDefined)
}
def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
@@ -276,6 +263,7 @@ trait IssuesService {
// Search Issue
val issues = Issues
.filter(_.byRepository(owner, repository))
.innerJoin(IssueOutline).on { case (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
}
@@ -291,6 +279,7 @@ trait IssuesService {
// Search IssueComment
val comments = IssueComments
.filter(_.byRepository(owner, repository))
.innerJoin(Issues).on { case (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
}
@@ -336,31 +325,62 @@ object IssuesService {
milestoneId: Option[Option[Int]] = None,
author: Option[String] = None,
assigned: Option[String] = None,
repo: Option[String] = None,
mentioned: Option[String] = None,
state: String = "open",
sort: String = "created",
direction: String = "desc"){
direction: String = "desc",
visibility: Option[String] = None,
groups: Set[String] = Set.empty){
def isEmpty: Boolean = {
labels.isEmpty && milestoneId.isEmpty && author.isEmpty && assigned.isEmpty &&
state == "open" && sort == "created" && direction == "desc"
state == "open" && sort == "created" && direction == "desc" && visibility.isEmpty
}
def nonEmpty: Boolean = !isEmpty
def toFilterString: String = (
List(
Some(s"is:${state}"),
author.map(author => s"author:${author}"),
assigned.map(assignee => s"assignee:${assignee}"),
mentioned.map(mentioned => s"mentions:${mentioned}")
).flatten ++
labels.map(label => s"label:${label}") ++
List(
milestoneId.map { _ match {
case Some(x) => s"milestone:${milestoneId}"
case None => "no:milestone"
}},
(sort, direction) match {
case ("created" , "desc") => None
case ("created" , "asc" ) => Some("sort:created-asc")
case ("comments", "desc") => Some("sort:comments-desc")
case ("comments", "asc" ) => Some("sort:comments-asc")
case ("updated" , "desc") => Some("sort:updated-desc")
case ("updated" , "asc" ) => Some("sort:updated-asc")
},
visibility.map(visibility => s"visibility:${visibility}")
).flatten ++
groups.map(group => s"group:${group}")
).mkString(" ")
def toURL: String =
"?" + List(
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
milestoneId.map { id => "milestone=" + (id match {
case Some(x) => x.toString
case None => "none"
})},
author .map(x => "author=" + urlEncode(x)),
assigned.map(x => "assigned=" + urlEncode(x)),
repo.map("for=" + urlEncode(_)),
milestoneId.map { _ match {
case Some(x) => "milestone=" + x
case None => "milestone=none"
}},
author .map(x => "author=" + urlEncode(x)),
assigned .map(x => "assigned=" + urlEncode(x)),
mentioned.map(x => "mentioned=" + urlEncode(x)),
Some("state=" + urlEncode(state)),
Some("sort=" + urlEncode(sort)),
Some("direction=" + urlEncode(direction))).flatten.mkString("&")
Some("direction=" + urlEncode(direction)),
visibility.map(x => "visibility=" + urlEncode(x)),
if(groups.isEmpty) None else Some("groups=" + urlEncode(groups.mkString(",")))
).flatten.mkString("&")
}
@@ -371,19 +391,63 @@ object IssuesService {
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value)
}
/**
* Restores IssueSearchCondition instance from filter query.
*/
def apply(filter: String, milestones: Map[String, Int]): IssueSearchCondition = {
val conditions = filter.split("[  \t]+").map { x =>
val dim = x.split(":")
dim(0) -> dim(1)
}.groupBy(_._1).map { case (key, values) =>
key -> values.map(_._2).toSeq
}
val (sort, direction) = conditions.get("sort").flatMap(_.headOption).getOrElse("created-desc") match {
case "created-asc" => ("created" , "asc" )
case "comments-desc" => ("comments", "desc")
case "comments-asc" => ("comments", "asc" )
case "updated-desc" => ("comments", "desc")
case "updated-asc" => ("comments", "asc" )
case _ => ("created" , "desc")
}
IssueSearchCondition(
conditions.get("label").map(_.toSet).getOrElse(Set.empty),
conditions.get("milestone").flatMap(_.headOption) match {
case None => None
case Some("none") => Some(None)
case Some(x) => milestones.get(x).map(x => Some(x))
},
conditions.get("author").flatMap(_.headOption),
conditions.get("assignee").flatMap(_.headOption),
conditions.get("mentions").flatMap(_.headOption),
conditions.get("is").getOrElse(Seq.empty).find(x => x == "open" || x == "closed").getOrElse("open"),
sort,
direction,
conditions.get("visibility").flatMap(_.headOption),
conditions.get("group").map(_.toSet).getOrElse(Set.empty)
)
}
/**
* Restores IssueSearchCondition instance from request parameters.
*/
def apply(request: HttpServletRequest): IssueSearchCondition =
IssueSearchCondition(
param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
param(request, "milestone").map{
param(request, "milestone").map {
case "none" => None
case x => x.toIntOpt
},
param(request, "author"),
param(request, "assigned"),
param(request, "for"),
param(request, "mentioned"),
param(request, "state", Seq("open", "closed")).getOrElse("open"),
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"))
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"),
param(request, "visibility"),
param(request, "groups").map(_.split(",").toSet).getOrElse(Set.empty)
)
def page(request: HttpServletRequest) = try {
val i = param(request, "page").getOrElse("1").toInt

View File

@@ -3,6 +3,7 @@ package service
import model.Profile._
import profile.simple._
import model.{PullRequest, Issue}
import util.JGitUtil
trait PullRequestService { self: IssuesService =>
import PullRequestService._
@@ -36,23 +37,23 @@ trait PullRequestService { self: IssuesService =>
.list
.map { x => PullRequestCount(x._1, x._2) }
def getAllPullRequestCountGroupByUser(closed: Boolean, userName: String)(implicit s: Session): List[PullRequestCount] =
PullRequests
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
.innerJoin(Repositories).on { case ((t1, t2), t3) => t2.byRepository(t3.userName, t3.repositoryName) }
.filter { case ((t1, t2), t3) =>
(t2.closed === closed.bind) &&
(
(t3.isPrivate === false.bind) ||
(t3.userName === userName.bind) ||
(Collaborators.filter { t4 => t4.byRepository(t3.userName, t3.repositoryName) && (t4.collaboratorName === userName.bind)} exists)
)
}
.groupBy { case ((t1, t2), t3) => t2.openedUserName }
.map { case (userName, t) => userName -> t.length }
.sortBy(_._2 desc)
.list
.map { x => PullRequestCount(x._1, x._2) }
// def getAllPullRequestCountGroupByUser(closed: Boolean, userName: String)(implicit s: Session): List[PullRequestCount] =
// PullRequests
// .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
// .innerJoin(Repositories).on { case ((t1, t2), t3) => t2.byRepository(t3.userName, t3.repositoryName) }
// .filter { case ((t1, t2), t3) =>
// (t2.closed === closed.bind) &&
// (
// (t3.isPrivate === false.bind) ||
// (t3.userName === userName.bind) ||
// (Collaborators.filter { t4 => t4.byRepository(t3.userName, t3.repositoryName) && (t4.collaboratorName === userName.bind)} exists)
// )
// }
// .groupBy { case ((t1, t2), t3) => t2.openedUserName }
// .map { case (userName, t) => userName -> t.length }
// .sortBy(_._2 desc)
// .list
// .map { x => PullRequestCount(x._1, x._2) }
def createPullRequest(originUserName: String, originRepositoryName: String, issueId: Int,
originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String,
@@ -81,6 +82,18 @@ trait PullRequestService { self: IssuesService =>
.map { case (t1, t2) => t1 }
.list
/**
* Fetch pull request contents into refs/pull/${issueId}/head and update pull request table.
*/
def updatePullRequests(owner: String, repository: String, branch: String)(implicit s: Session): Unit =
getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq =>
if(Repositories.filter(_.byRepository(pullreq.userName, pullreq.repositoryName)).exists.run){
val (commitIdTo, commitIdFrom) = JGitUtil.updatePullRequest(
pullreq.userName, pullreq.repositoryName, pullreq.branch, pullreq.issueId,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch)
updateCommitId(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo, commitIdFrom)
}
}
}
object PullRequestService {

View File

@@ -20,6 +20,7 @@ trait RepositorySearchService { self: IssuesService =>
searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) =>
IssueSearchResult(
issue.issueId,
issue.isPullRequest,
issue.title,
issue.openedUserName,
issue.registeredDate,
@@ -111,6 +112,7 @@ object RepositorySearchService {
case class IssueSearchResult(
issueId: Int,
isPullRequest: Boolean,
title: String,
openedUserName: String,
registeredDate: java.util.Date,

View File

@@ -2,7 +2,7 @@ package service
import model.Profile._
import profile.simple._
import model.{Repository, Account, Collaborator}
import model.{Repository, Account, Collaborator, Label}
import util.JGitUtil
trait RepositoryService { self: AccountService =>
@@ -46,16 +46,16 @@ trait RepositoryService { self: AccountService =>
(Repositories filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository =>
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueComments = IssueComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val activities = Activities .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val collaborators = Collaborators.filter(_.byRepository(oldUserName, oldRepositoryName)).list
val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueComments = IssueComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val commitComments = CommitComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list
val collaborators = Collaborators .filter(_.byRepository(oldUserName, oldRepositoryName)).list
Repositories.filter { t =>
(t.originUserName === oldUserName.bind) && (t.originRepositoryName === oldRepositoryName.bind)
@@ -69,11 +69,18 @@ trait RepositoryService { self: AccountService =>
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 =>
Activities.filter(_.activityId === activity.activityId.bind)
.map(x => (x.userName, x.repositoryName)).update(newUserName, newRepositoryName)
}
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)) :_*)
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(
@@ -87,8 +94,17 @@ trait RepositoryService { self: AccountService =>
PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueLabels .insertAll(issueLabels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Activities .insertAll(activities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
CommitComments.insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = 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
IssueLabels.insertAll(issueLabels.map(x => x.copy(
labelId = newLabelMap(oldLabelMap(x.labelId)),
userName = newUserName,
repositoryName = newRepositoryName
)) :_*)
if(account.isGroupAccount){
Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*)
} else {
@@ -96,12 +112,11 @@ trait RepositoryService { self: AccountService =>
}
// Update activity messages
val updateActivities = Activities.filter { t =>
Activities.filter { t =>
(t.message like s"%:${oldUserName}/${oldRepositoryName}]%") ||
(t.message like s"%:${oldUserName}/${oldRepositoryName}#%")
}.map { t => t.activityId -> t.message }.list
updateActivities.foreach { case (activityId, message) =>
(t.message like s"%:${oldUserName}/${oldRepositoryName}#%") ||
(t.message like s"%:${oldUserName}/${oldRepositoryName}@%")
}.map { t => t.activityId -> t.message }.list.foreach { case (activityId, message) =>
Activities.filter(_.activityId === activityId.bind).map(_.message).update(
message
.replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]")
@@ -109,6 +124,7 @@ trait RepositoryService { self: AccountService =>
.replace(s"[tag:${oldUserName}/${oldRepositoryName}#" ,s"[tag:${newUserName}/${newRepositoryName}#")
.replace(s"[pullreq:${oldUserName}/${oldRepositoryName}#",s"[pullreq:${newUserName}/${newRepositoryName}#")
.replace(s"[issue:${oldUserName}/${oldRepositoryName}#" ,s"[issue:${newUserName}/${newRepositoryName}#")
.replace(s"[commit:${oldUserName}/${oldRepositoryName}@" ,s"[commit:${newUserName}/${newRepositoryName}@")
)
}
}
@@ -118,6 +134,7 @@ trait RepositoryService { self: AccountService =>
def deleteRepository(userName: String, repositoryName: String)(implicit s: Session): Unit = {
Activities .filter(_.byRepository(userName, repositoryName)).delete
Collaborators .filter(_.byRepository(userName, repositoryName)).delete
CommitComments.filter(_.byRepository(userName, repositoryName)).delete
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
Labels .filter(_.byRepository(userName, repositoryName)).delete
IssueComments .filter(_.byRepository(userName, repositoryName)).delete
@@ -127,6 +144,30 @@ trait RepositoryService { self: AccountService =>
Milestones .filter(_.byRepository(userName, repositoryName)).delete
WebHooks .filter(_.byRepository(userName, repositoryName)).delete
Repositories .filter(_.byRepository(userName, repositoryName)).delete
// Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME
Repositories
.filter { x => (x.originUserName === userName.bind) && (x.originRepositoryName === repositoryName.bind) }
.map { x => (x.userName, x.repositoryName) }
.list
.foreach { case (userName, repositoryName) =>
Repositories
.filter(_.byRepository(userName, repositoryName))
.map(x => (x.originUserName?, x.originRepositoryName?))
.update(None, None)
}
// Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME
Repositories
.filter { x => (x.parentUserName === userName.bind) && (x.parentRepositoryName === repositoryName.bind) }
.map { x => (x.userName, x.repositoryName) }
.list
.foreach { case (userName, repositoryName) =>
Repositories
.filter(_.byRepository(userName, repositoryName))
.map(x => (x.parentUserName?, x.parentRepositoryName?))
.update(None, None)
}
}
/**
@@ -156,8 +197,8 @@ trait RepositoryService { self: AccountService =>
new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
repository,
issues.size,
issues.filter(_ == true).size,
issues.count(_ == false),
issues.count(_ == true),
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
@@ -355,4 +396,4 @@ object RepositoryService {
case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode])
}
}

View File

@@ -12,7 +12,10 @@ trait SystemSettingsService {
def saveSystemSettings(settings: SystemSettings): Unit = {
defining(new java.util.Properties()){ props =>
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
settings.information.foreach(x => props.setProperty(Information, x))
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
props.setProperty(AllowAnonymousAccess, settings.allowAnonymousAccess.toString)
props.setProperty(IsCreateRepoOptionPublic, settings.isCreateRepoOptionPublic.toString)
props.setProperty(Gravatar, settings.gravatar.toString)
props.setProperty(Notification, settings.notification.toString)
props.setProperty(Ssh, settings.ssh.toString)
@@ -39,8 +42,9 @@ trait SystemSettingsService {
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
ldap.additionalFilterCondition.foreach(x => props.setProperty(LdapAdditionalFilterCondition, x))
ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
ldap.mailAttribute.foreach(x => props.setProperty(LdapMailAddressAttribute, x.toString))
ldap.mailAttribute.foreach(x => props.setProperty(LdapMailAddressAttribute, x))
ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
ldap.ssl.foreach(x => props.setProperty(LdapSsl, x.toString))
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
}
}
@@ -60,7 +64,10 @@ trait SystemSettingsService {
}
SystemSettings(
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
getOptionValue[String](props, Information, None),
getValue(props, AllowAccountRegistration, false),
getValue(props, AllowAnonymousAccess, true),
getValue(props, IsCreateRepoOptionPublic, true),
getValue(props, Gravatar, true),
getValue(props, Notification, false),
getValue(props, Ssh, false),
@@ -90,6 +97,7 @@ trait SystemSettingsService {
getOptionValue(props, LdapFullNameAttribute, None),
getOptionValue(props, LdapMailAddressAttribute, None),
getOptionValue[Boolean](props, LdapTls, None),
getOptionValue[Boolean](props, LdapSsl, None),
getOptionValue(props, LdapKeystore, None)))
} else {
None
@@ -105,7 +113,10 @@ object SystemSettingsService {
case class SystemSettings(
baseUrl: Option[String],
information: Option[String],
allowAccountRegistration: Boolean,
allowAnonymousAccess: Boolean,
isCreateRepoOptionPublic: Boolean,
gravatar: Boolean,
notification: Boolean,
ssh: Boolean,
@@ -131,6 +142,7 @@ object SystemSettingsService {
fullNameAttribute: Option[String],
mailAttribute: Option[String],
tls: Option[Boolean],
ssl: Option[Boolean],
keystore: Option[String])
case class Smtp(
@@ -147,7 +159,10 @@ object SystemSettingsService {
val DefaultLdapPort = 389
private val BaseURL = "base_url"
private val Information = "information"
private val AllowAccountRegistration = "allow_account_registration"
private val AllowAnonymousAccess = "allow_anonymous_access"
private val IsCreateRepoOptionPublic = "is_create_repository_option_public"
private val Gravatar = "gravatar"
private val Notification = "notification"
private val Ssh = "ssh"
@@ -170,6 +185,7 @@ object SystemSettingsService {
private val LdapFullNameAttribute = "ldap.fullname_attribute"
private val LdapMailAddressAttribute = "ldap.mail_attribute"
private val LdapTls = "ldap.tls"
private val LdapSsl = "ldap.ssl"
private val LdapKeystore = "ldap.keystore"
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
@@ -191,7 +207,7 @@ object SystemSettingsService {
else value
}
// TODO temporary flag
val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean
// // TODO temporary flag
// val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean
}

View File

@@ -8,10 +8,9 @@ import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory
import util.Directory._
import util.ControlUtil._
import util.JDBCUtil._
import org.eclipse.jgit.api.Git
import util.Directory
import plugin.PluginUpdateJob
import service.SystemSettingsService
object AutoUpdate {
@@ -53,22 +52,54 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
new Version(2, 8),
new Version(2, 7) {
override def update(conn: Connection): Unit = {
super.update(conn)
conn.select("SELECT * FROM REPOSITORY"){ rs =>
// Rename attached files directory from /issues to /comments
val userName = rs.getString("USER_NAME")
val repoName = rs.getString("REPOSITORY_NAME")
defining(Directory.getAttachedDir(userName, repoName)){ newDir =>
val oldDir = new File(newDir.getParentFile, "issues")
if(oldDir.exists && oldDir.isDirectory){
oldDir.renameTo(newDir)
}
}
// Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME if it does not exist
val originalUserName = rs.getString("ORIGIN_USER_NAME")
val originalRepoName = rs.getString("ORIGIN_REPOSITORY_NAME")
if(originalUserName != null && originalRepoName != null){
if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
originalUserName, originalRepoName) == 0){
conn.update("UPDATE REPOSITORY SET ORIGIN_USER_NAME = NULL, ORIGIN_REPOSITORY_NAME = NULL " +
"WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
}
}
// Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME if it does not exist
val parentUserName = rs.getString("PARENT_USER_NAME")
val parentRepoName = rs.getString("PARENT_REPOSITORY_NAME")
if(parentUserName != null && parentRepoName != null){
if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
parentUserName, parentRepoName) == 0){
conn.update("UPDATE REPOSITORY SET PARENT_USER_NAME = NULL, PARENT_REPOSITORY_NAME = NULL " +
"WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
}
}
}
}
},
new Version(2, 6),
new Version(2, 5),
new Version(2, 4),
new Version(2, 3) {
override def update(conn: Connection): Unit = {
super.update(conn)
using(conn.createStatement.executeQuery("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'")){ rs =>
while(rs.next) {
val info = rs.getString("ADDITIONAL_INFO")
val newInfo = info.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n")
if (info != newInfo) {
val id = rs.getString("ACTIVITY_ID")
using(conn.prepareStatement("UPDATE ACTIVITY SET ADDITIONAL_INFO=? WHERE ACTIVITY_ID=?")) { sql =>
sql.setString(1, newInfo)
sql.setLong(2, id.toLong)
sql.executeUpdate
}
}
conn.select("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'"){ rs =>
val curInfo = rs.getString("ADDITIONAL_INFO")
val newInfo = curInfo.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n")
if (curInfo != newInfo) {
conn.update("UPDATE ACTIVITY SET ADDITIONAL_INFO = ? WHERE ACTIVITY_ID = ?", newInfo, rs.getInt("ACTIVITY_ID"))
}
}
FileUtils.deleteDirectory(Directory.getPluginCacheDir())
@@ -85,16 +116,14 @@ object AutoUpdate {
mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
super.update(conn)
using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
while(rs.next){
defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
if(dir.exists && dir.isDirectory){
dir.listFiles.foreach { file =>
if(file.getName.indexOf('.') < 0){
val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString
if(mimeType.startsWith("image/")){
file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1)))
}
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
if(dir.exists && dir.isDirectory){
dir.listFiles.foreach { file =>
if(file.getName.indexOf('.') < 0){
val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString
if(mimeType.startsWith("image/")){
file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1)))
}
}
}
@@ -117,14 +146,12 @@ object AutoUpdate {
override def update(conn: Connection): Unit = {
super.update(conn)
// Fix wiki repository configuration
using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
while(rs.next){
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
defining(git.getRepository.getConfig){ config =>
if(!config.getBoolean("http", "receivepack", false)){
config.setBoolean("http", null, "receivepack", true)
config.save
}
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
defining(git.getRepository.getConfig){ config =>
if(!config.getBoolean("http", "receivepack", false)){
config.setBoolean("http", null, "receivepack", true)
config.save
}
}
}
@@ -169,11 +196,10 @@ object AutoUpdate {
* Update database schema automatically in the context initializing.
*/
class AutoUpdateListener extends ServletContextListener {
import org.quartz.impl.StdSchedulerFactory
import AutoUpdate._
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
private val scheduler = StdSchedulerFactory.getDefaultScheduler
// private val scheduler = StdSchedulerFactory.getDefaultScheduler
override def contextInitialized(event: ServletContextEvent): Unit = {
val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
@@ -208,31 +234,9 @@ class AutoUpdateListener extends ServletContextListener {
}
logger.debug("End schema update")
}
if(SystemSettingsService.enablePluginSystem){
getDatabase(context).withSession { implicit session =>
logger.debug("Starting plugin system...")
try {
plugin.PluginSystem.init()
scheduler.start()
PluginUpdateJob.schedule(scheduler)
logger.debug("PluginUpdateJob is started.")
logger.debug("Plugin system is initialized.")
} catch {
case ex: Throwable => {
logger.error("Failed to initialize plugin system", ex)
ex.printStackTrace()
throw ex
}
}
}
}
}
def contextDestroyed(sce: ServletContextEvent): Unit = {
scheduler.shutdown()
}
private def getConnection(servletContext: ServletContext): Connection =

View File

@@ -28,33 +28,45 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
override def setCharacterEncoding(encoding: String) = {}
}
val isUpdating = request.getRequestURI.endsWith("/git-receive-pack") || "service=git-receive-pack".equals(request.getQueryString)
val settings = loadSystemSettings()
try {
defining(request.paths){ case Array(_, repositoryOwner, repositoryName, _*) =>
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match {
case Some(repository) => {
if(!request.getRequestURI.endsWith("/git-receive-pack") &&
!"service=git-receive-pack".equals(request.getQueryString) && !repository.repository.isPrivate){
chain.doFilter(req, wrappedResponse)
} else {
request.getHeader("Authorization") match {
case null => requireAuth(response)
case auth => decodeAuthHeader(auth).split(":") match {
case Array(username, password) => getWritableUser(username, password, repository) match {
case Some(account) => {
request.setAttribute(Keys.Request.UserName, account.userName)
chain.doFilter(req, wrappedResponse)
defining(request.paths){
case Array(_, repositoryOwner, repositoryName, _*) =>
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match {
case Some(repository) => {
if(!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess){
chain.doFilter(req, wrappedResponse)
} else {
request.getHeader("Authorization") match {
case null => requireAuth(response)
case auth => decodeAuthHeader(auth).split(":") match {
case Array(username, password) => {
authenticate(settings, username, password) match {
case Some(account) => {
if(isUpdating && hasWritePermission(repository.owner, repository.name, Some(account))){
request.setAttribute(Keys.Request.UserName, account.userName)
}
chain.doFilter(req, wrappedResponse)
}
case None => requireAuth(response)
}
}
case None => requireAuth(response)
case _ => requireAuth(response)
}
case _ => requireAuth(response)
}
}
}
case None => {
logger.debug(s"Repository ${repositoryOwner}/${repositoryName} is not found.")
response.sendError(HttpServletResponse.SC_NOT_FOUND)
}
}
case None => {
logger.debug(s"Repository ${repositoryOwner}/${repositoryName} is not found.")
response.sendError(HttpServletResponse.SC_NOT_FOUND)
}
case _ => {
logger.debug(s"Not enough path arguments: ${request.paths}")
response.sendError(HttpServletResponse.SC_NOT_FOUND)
}
}
} catch {
@@ -65,13 +77,6 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
}
}
private def getWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo)
(implicit session: Session): Option[Account] =
authenticate(loadSystemSettings(), username, password) match {
case x @ Some(account) if(hasWritePermission(repository.owner, repository.name, x)) => x
case _ => None
}
private def requireAuth(response: HttpServletResponse): Unit = {
response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"")
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)

View File

@@ -174,7 +174,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
case ReceiveCommand.Type.CREATE |
ReceiveCommand.Type.UPDATE |
ReceiveCommand.Type.UPDATE_NONFASTFORWARD =>
updatePullRequests(branchName)
updatePullRequests(owner, repository, branchName)
case _ =>
}
}
@@ -211,26 +211,4 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
}
}
}
/**
* Fetch pull request contents into refs/pull/${issueId}/head and update pull request table.
*/
private def updatePullRequests(branch: String) =
getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq =>
if(getRepository(pullreq.userName, pullreq.repositoryName, baseUrl).isDefined){
using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName)),
Git.open(Directory.getRepositoryDir(pullreq.requestUserName, pullreq.requestRepositoryName))){ (oldGit, newGit) =>
oldGit.fetch
.setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/pull/${pullreq.issueId}/head").setForceUpdate(true))
.call
val commitIdTo = oldGit.getRepository.resolve(s"refs/pull/${pullreq.issueId}/head").getName
val commitIdFrom = JGitUtil.getForkedCommitId(oldGit, newGit,
pullreq.userName, pullreq.repositoryName, pullreq.branch,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch)
updateCommitId(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo, commitIdFrom)
}
}
}
}

View File

@@ -1,192 +0,0 @@
package servlet
import javax.servlet._
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import org.apache.commons.io.IOUtils
import play.twirl.api.Html
import service.{AccountService, RepositoryService, SystemSettingsService}
import model.{Account, Session}
import util.{JGitUtil, Keys}
import plugin.{RawData, Fragment, PluginConnectionHolder, Redirect}
import service.RepositoryService.RepositoryInfo
import plugin.Security._
class PluginActionInvokeFilter extends Filter with SystemSettingsService with RepositoryService with AccountService {
def init(config: FilterConfig) = {}
def destroy(): Unit = {}
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
(req, res) match {
case (request: HttpServletRequest, response: HttpServletResponse) => {
Database(req.getServletContext) withTransaction { implicit session =>
val path = request.getRequestURI.substring(request.getServletContext.getContextPath.length)
if(!processGlobalAction(path, request, response) && !processRepositoryAction(path, request, response)){
chain.doFilter(req, res)
}
}
}
}
}
private def processGlobalAction(path: String, request: HttpServletRequest, response: HttpServletResponse)
(implicit session: Session): Boolean = {
plugin.PluginSystem.globalActions.find(x =>
x.method.toLowerCase == request.getMethod.toLowerCase && path.matches(x.path)
).map { action =>
val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
val systemSettings = loadSystemSettings()
implicit val context = app.Context(systemSettings, Option(loginAccount), request)
if(authenticate(action.security, context)){
val result = try {
PluginConnectionHolder.threadLocal.set(session.conn)
action.function(request, response, context)
} finally {
PluginConnectionHolder.threadLocal.remove()
}
processActionResult(result, request, response, context)
} else {
// TODO NotFound or Error?
}
true
} getOrElse false
}
private def processRepositoryAction(path: String, request: HttpServletRequest, response: HttpServletResponse)
(implicit session: Session): Boolean = {
val elements = path.split("/")
if(elements.length > 3){
val owner = elements(1)
val name = elements(2)
val remain = elements.drop(3).mkString("/", "/", "")
val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
val systemSettings = loadSystemSettings()
implicit val context = app.Context(systemSettings, Option(loginAccount), request)
getRepository(owner, name, systemSettings.baseUrl(request)).flatMap { repository =>
plugin.PluginSystem.repositoryActions.find(x => remain.matches(x.path)).map { action =>
if(authenticate(action.security, context, repository)){
val result = try {
PluginConnectionHolder.threadLocal.set(session.conn)
action.function(request, response, context, repository)
} finally {
PluginConnectionHolder.threadLocal.remove()
}
processActionResult(result, request, response, context)
} else {
// TODO NotFound or Error?
}
true
}
} getOrElse false
} else false
}
private def processActionResult(result: Any, request: HttpServletRequest, response: HttpServletResponse,
context: app.Context): Unit = {
result match {
case null|None => renderError(request, response, context, 404)
case x: String => renderGlobalHtml(request, response, context, x)
case Some(x: String) => renderGlobalHtml(request, response, context, x)
case x: Html => renderGlobalHtml(request, response, context, x.toString)
case Some(x: Html) => renderGlobalHtml(request, response, context, x.toString)
case x: Fragment => renderFragmentHtml(request, response, context, x.html.toString)
case Some(x: Fragment) => renderFragmentHtml(request, response, context, x.html.toString)
case x: RawData => renderRawData(request, response, context, x)
case Some(x: RawData) => renderRawData(request, response, context, x)
case x: Redirect => response.sendRedirect(x.path)
case Some(x: Redirect) => response.sendRedirect(x.path)
case x: AnyRef => renderJson(request, response, x)
}
}
/**
* Authentication for global action
*/
private def authenticate(security: Security, context: app.Context)(implicit session: Session): Boolean = {
// Global Action
security match {
case All() => true
case Login() => context.loginAccount.isDefined
case Admin() => context.loginAccount.exists(_.isAdmin)
case _ => false // TODO throw Exception?
}
}
/**
* Authenticate for repository action
*/
private def authenticate(security: Security, context: app.Context, repository: RepositoryInfo)(implicit session: Session): Boolean = {
if(repository.repository.isPrivate){
// Private Repository
security match {
case Admin() => context.loginAccount.exists(_.isAdmin)
case Owner() => context.loginAccount.exists { account =>
account.userName == repository.owner ||
getGroupMembers(repository.owner).exists(m => m.userName == account.userName && m.isManager)
}
case _ => context.loginAccount.exists { account =>
account.isAdmin || account.userName == repository.owner ||
getCollaborators(repository.owner, repository.name).contains(account.userName)
}
}
} else {
// Public Repository
security match {
case All() => true
case Login() => context.loginAccount.isDefined
case Owner() => context.loginAccount.exists { account =>
account.userName == repository.owner ||
getGroupMembers(repository.owner).exists(m => m.userName == account.userName && m.isManager)
}
case Member() => context.loginAccount.exists { account =>
account.userName == repository.owner ||
getCollaborators(repository.owner, repository.name).contains(account.userName)
}
case Admin() => context.loginAccount.exists(_.isAdmin)
}
}
}
private def renderError(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, error: Int): Unit = {
response.sendError(error)
}
private def renderGlobalHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, body: String): Unit = {
response.setContentType("text/html; charset=UTF-8")
val html = _root_.html.main("GitBucket", None)(Html(body))(context)
IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
}
private def renderRepositoryHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, repository: RepositoryInfo, body: String): Unit = {
response.setContentType("text/html; charset=UTF-8")
val html = _root_.html.main("GitBucket", None)(_root_.html.menu("", repository)(Html(body))(context))(context) // TODO specify active side menu
IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
}
private def renderFragmentHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, body: String): Unit = {
response.setContentType("text/html; charset=UTF-8")
IOUtils.write(body.getBytes("UTF-8"), response.getOutputStream)
}
private def renderRawData(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, rawData: RawData): Unit = {
response.setContentType(rawData.contentType)
IOUtils.write(rawData.content, response.getOutputStream)
}
private def renderJson(request: HttpServletRequest, response: HttpServletResponse, obj: AnyRef): Unit = {
import org.json4s._
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.write
implicit val formats = Serialization.formats(NoTypeHints)
val json = write(obj)
response.setContentType("application/json; charset=UTF-8")
IOUtils.write(json.getBytes("UTF-8"), response.getOutputStream)
}
}

View File

@@ -48,7 +48,7 @@ object Directory {
* Directory for files which are attached to issue.
*/
def getAttachedDir(owner: String, repository: String): File =
new File(s"${RepositoryHome}/${owner}/${repository}/issues")
new File(s"${RepositoryHome}/${owner}/${repository}/comments")
/**
* Directory for uploaded files by the specified user.

View File

@@ -0,0 +1,55 @@
package util
import java.sql._
import util.ControlUtil._
import scala.collection.mutable.ListBuffer
/**
* Provides implicit class which extends java.sql.Connection.
* This is used in automatic migration in [[servlet.AutoUpdateListener]].
*/
object JDBCUtil {
implicit class RichConnection(conn: Connection){
def update(sql: String, params: Any*): Int = {
execute(sql, params: _*){ stmt =>
stmt.executeUpdate()
}
}
def select[T](sql: String, params: Any*)(f: ResultSet => T): Seq[T] = {
execute(sql, params: _*){ stmt =>
using(stmt.executeQuery()){ rs =>
val list = new ListBuffer[T]
while(rs.next){
list += f(rs)
}
list.toSeq
}
}
}
def selectInt(sql: String, params: Any*): Int = {
execute(sql, params: _*){ stmt =>
using(stmt.executeQuery()){ rs =>
if(rs.next) rs.getInt(1) else 0
}
}
}
private def execute[T](sql: String, params: Any*)(f: (PreparedStatement) => T): T = {
using(conn.prepareStatement(sql)){ stmt =>
params.zipWithIndex.foreach { case (p, i) =>
p match {
case x: Int => stmt.setInt(i + 1, x)
case x: String => stmt.setString(i + 1, x)
}
}
f(stmt)
}
}
}
}

View File

@@ -13,8 +13,9 @@ import org.eclipse.jgit.treewalk._
import org.eclipse.jgit.treewalk.filter._
import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException}
import org.eclipse.jgit.transport.RefSpec
import java.util.Date
import org.eclipse.jgit.api.errors.NoHeadException
import org.eclipse.jgit.api.errors.{JGitInternalException, InvalidRefNameException, RefAlreadyExistsException, NoHeadException}
import service.RepositoryService
import org.eclipse.jgit.dircache.DirCacheEntry
import org.slf4j.LoggerFactory
@@ -217,7 +218,7 @@ object JGitUtil {
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl))
}
list = list.map(tuple =>
list.transform(tuple =>
if (tuple._2 != FileMode.TREE)
tuple
else
@@ -507,6 +508,17 @@ object JGitUtil {
}.find(_._1 != null)
}
def createBranch(git: Git, fromBranch: String, newBranch: String) = {
try {
git.branchCreate().setStartPoint(fromBranch).setName(newBranch).call()
Right("Branch created.")
} catch {
case e: RefAlreadyExistsException => Left("Sorry, that branch already exists.")
// JGitInternalException occurs when new branch name is 'a' and the branch whose name is 'a/*' exists.
case _: InvalidRefNameException | _: JGitInternalException => Left("Sorry, that name is invalid.")
}
}
def createDirCacheEntry(path: String, mode: FileMode, objectId: ObjectId): DirCacheEntry = {
val entry = new DirCacheEntry(path)
entry.setFileMode(mode)
@@ -663,6 +675,25 @@ object JGitUtil {
}.head.id
}
/**
* Fetch pull request contents into refs/pull/${issueId}/head and return (commitIdTo, commitIdFrom)
*/
def updatePullRequest(userName: String, repositoryName:String, branch: String, issueId: Int,
requestUserName: String, requestRepositoryName: String, requestBranch: String):(String, String) =
using(Git.open(Directory.getRepositoryDir(userName, repositoryName)),
Git.open(Directory.getRepositoryDir(requestUserName, requestRepositoryName))){ (oldGit, newGit) =>
oldGit.fetch
.setRemote(Directory.getRepositoryDir(requestUserName, requestRepositoryName).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${requestBranch}:refs/pull/${issueId}/head").setForceUpdate(true))
.call
val commitIdTo = oldGit.getRepository.resolve(s"refs/pull/${issueId}/head").getName
val commitIdFrom = getForkedCommitId(oldGit, newGit,
userName, repositoryName, branch,
requestUserName, requestRepositoryName, requestBranch)
(commitIdTo, commitIdFrom)
}
/**
* Returns the last modified commit of specified path
* @param git the Git object

View File

@@ -48,6 +48,7 @@ object LDAPUtil {
dn = ldapSettings.bindDN.getOrElse(""),
password = ldapSettings.bindPassword.getOrElse(""),
tls = ldapSettings.tls.getOrElse(false),
ssl = ldapSettings.ssl.getOrElse(false),
keystore = ldapSettings.keystore.getOrElse(""),
error = "System LDAP authentication failed."
){ conn =>
@@ -65,6 +66,7 @@ object LDAPUtil {
dn = userDN,
password = password,
tls = ldapSettings.tls.getOrElse(false),
ssl = ldapSettings.ssl.getOrElse(false),
keystore = ldapSettings.keystore.getOrElse(""),
error = "User LDAP Authentication Failed."
){ conn =>
@@ -96,7 +98,7 @@ object LDAPUtil {
}).replaceAll("[^a-zA-Z0-9\\-_.]", "").replaceAll("^[_\\-]", "")
}
private def bind[A](host: String, port: Int, dn: String, password: String, tls: Boolean, keystore: String, error: String)
private def bind[A](host: String, port: Int, dn: String, password: String, tls: Boolean, ssl: Boolean, keystore: String, error: String)
(f: LDAPConnection => Either[String, A]): Either[String, A] = {
if (tls) {
// Dynamically set Sun as the security provider
@@ -109,7 +111,13 @@ object LDAPUtil {
}
}
val conn: LDAPConnection = new LDAPConnection(new LDAPJSSEStartTLSFactory())
val conn: LDAPConnection =
if(ssl) {
new LDAPConnection(new LDAPJSSESecureSocketFactory())
}else {
new LDAPConnection(new LDAPJSSEStartTLSFactory())
}
try {
// Connect to the server
conn.connect(host, port)

View File

@@ -12,7 +12,7 @@ trait LinkConverter { self: RequestCache =>
issueIdPrefix: String = "#")(implicit context: app.Context): String = {
value
// escape HTML tags
.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;")
.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
// convert issue id to link
.replaceBy(("(?<=(^|\\W))" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r){ m =>
getIssue(repository.owner, repository.name, m.group(2)) match {

View File

@@ -9,6 +9,7 @@ import org.pegdown.ast._
import org.pegdown.LinkRenderer.Rendering
import java.text.Normalizer
import java.util.Locale
import java.util.regex.Pattern
import scala.collection.JavaConverters._
import service.{RequestCache, WikiService}
@@ -17,23 +18,38 @@ object Markdown {
/**
* Converts Markdown of Wiki pages to HTML.
*/
def toHtml(markdown: String, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): String = {
def toHtml(markdown: String,
repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
enableRefsLink: Boolean,
enableTaskList: Boolean = false,
hasWritePermission: Boolean = false,
pages: List[String] = Nil)(implicit context: app.Context): String = {
// escape issue id
val source = if(enableRefsLink){
val s = if(enableRefsLink){
markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2")
} else markdown
// escape task list
val source = if(enableTaskList){
GitBucketHtmlSerializer.escapeTaskList(s)
} else s
val rootNode = new PegDownProcessor(
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS | Extensions.SUPPRESS_ALL_HTML
).parseMarkdown(source.toCharArray)
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink).toHtml(rootNode)
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission, pages).toHtml(rootNode)
}
}
class GitBucketLinkRender(context: app.Context, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean) extends LinkRenderer with WikiService {
class GitBucketLinkRender(
context: app.Context,
repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
pages: List[String]) extends LinkRenderer with WikiService {
override def render(node: WikiLinkNode): Rendering = {
if(enableWikiLink){
try {
@@ -47,7 +63,7 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
val url = repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/" + StringUtil.urlEncode(page)
if(getWikiPage(repository.owner, repository.name, page).isDefined){
if(pages.contains(page)){
new Rendering(url, label)
} else {
new Rendering(url, label).withAttribute("class", "absent")
@@ -82,9 +98,12 @@ class GitBucketHtmlSerializer(
markdown: String,
repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
enableRefsLink: Boolean
enableRefsLink: Boolean,
enableTaskList: Boolean,
hasWritePermission: Boolean,
pages: List[String]
)(implicit val context: app.Context) extends ToHtmlSerializer(
new GitBucketLinkRender(context, repository, enableWikiLink),
new GitBucketLinkRender(context, repository, enableWikiLink, pages),
Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava
) with LinkConverter with RequestCache {
@@ -103,10 +122,10 @@ class GitBucketHtmlSerializer(
}
private def fixUrl(url: String, isImage: Boolean = false): String = {
if(!enableWikiLink){
if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#") || url.startsWith("/")){
url
} else if(context.currentPath.contains("/blob/")){
if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#") || url.startsWith("/")){
url
} else if(!enableWikiLink){
if(context.currentPath.contains("/blob/")){
url + (if(isImage) "?raw=true" else "")
} else if(context.currentPath.contains("/tree/")){
val paths = context.currentPath.split("/")
@@ -143,7 +162,10 @@ class GitBucketHtmlSerializer(
override def visit(node: TextNode): Unit = {
// convert commit id and username to link.
val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText
val t = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText
// convert task list to checkbox.
val text = if(enableTaskList) GitBucketHtmlSerializer.convertCheckBox(t, hasWritePermission) else t
if (abbreviations.isEmpty) {
printer.print(text)
@@ -151,6 +173,54 @@ class GitBucketHtmlSerializer(
printWithAbbreviations(text)
}
}
override def visit(node: BulletListNode): Unit = {
if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) {
printer.println().print("""<ul class="task-list">""").indent(+2)
visitChildren(node)
printer.indent(-2).println().print("</ul>")
} else {
printIndentedTag(node, "ul")
}
}
override def visit(node: ListItemNode): Unit = {
if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) {
printer.println()
printer.print("""<li class="task-list-item">""")
visitChildren(node)
printer.print("</li>")
} else {
printer.println()
printTag(node, "li")
}
}
override def visit(node: ExpLinkNode) {
printLink(linkRenderer.render(node, printLinkChildrenToString(node)))
}
def printLinkChildrenToString(node: SuperNode) = {
val priorPrinter = printer
printer = new Printer()
visitLinkChildren(node)
val result = printer.getString()
printer = priorPrinter
result
}
def visitLinkChildren(node: SuperNode) {
import scala.collection.JavaConversions._
node.getChildren.foreach(child => child match {
case node: ExpImageNode => visitLinkChild(node)
case node: SuperNode => visitLinkChildren(node)
case _ => child.accept(this)
})
}
def visitLinkChild(node: ExpImageNode) {
printer.print("<img src=\"").print(fixUrl(node.url, true)).print("\" alt=\"").printEncoded(printChildrenToString(node)).print("\"/>")
}
}
object GitBucketHtmlSerializer {
@@ -163,4 +233,14 @@ object GitBucketHtmlSerializer {
val noSpecialChars = StringUtil.urlEncode(normalized)
noSpecialChars.toLowerCase(Locale.ENGLISH)
}
def escapeTaskList(text: String): String = {
Pattern.compile("""^( *)- \[([x| ])\] """, Pattern.MULTILINE).matcher(text).replaceAll("$1* task:$2: ")
}
def convertCheckBox(text: String, hasWritePermission: Boolean): String = {
val disabled = if (hasWritePermission) "" else "disabled"
text.replaceAll("task:x:", """<input type="checkbox" class="task-list-item-checkbox" checked="checked" """ + disabled + "/>")
.replaceAll("task: :", """<input type="checkbox" class="task-list-item-checkbox" """ + disabled + "/>")
}
}

View File

@@ -1,5 +1,5 @@
package view
import java.util.{Date, TimeZone}
import java.util.{Locale, Date, TimeZone}
import java.text.SimpleDateFormat
import play.twirl.api.Html
import util.StringUtil
@@ -15,6 +15,45 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
*/
def datetime(date: Date): String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date)
val timeUnits = List(
(1000L, "second"),
(1000L * 60, "minute"),
(1000L * 60 * 60, "hour"),
(1000L * 60 * 60 * 24, "day"),
(1000L * 60 * 60 * 24 * 30, "month"),
(1000L * 60 * 60 * 24 * 365, "year")
).reverse
/**
* Format java.util.Date to "x {seconds/minutes/hours/days/months/years} ago"
*/
def datetimeAgo(date: Date): String = {
val duration = new Date().getTime - date.getTime
timeUnits.find(tuple => duration / tuple._1 > 0) match {
case Some((unitValue, unitString)) =>
val value = duration / unitValue
s"${value} ${unitString}${if (value > 1) "s" else ""} ago"
case None => "just now"
}
}
/**
* Format java.util.Date to "x {seconds/minutes/hours/days} ago"
* If duration over 1 month, format to "d MMM (yyyy)"
*/
def datetimeAgoRecentOnly(date: Date): String = {
val duration = new Date().getTime - date.getTime
timeUnits.find(tuple => duration / tuple._1 > 0) match {
case Some((_, "month")) => s"on ${new SimpleDateFormat("d MMM", Locale.ENGLISH).format(date)}"
case Some((_, "year")) => s"on ${new SimpleDateFormat("d MMM yyyy", Locale.ENGLISH).format(date)}"
case Some((unitValue, unitString)) =>
val value = duration / unitValue
s"${value} ${unitString}${if (value > 1) "s" else ""} ago"
case None => "just now"
}
}
/**
* Format java.util.Date to "yyyy-MM-dd'T'hh:mm:ss'Z'".
*/
@@ -38,7 +77,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
private[this] val renderersBySuffix: Seq[(String, (List[String], String, String, service.RepositoryService.RepositoryInfo, Boolean, Boolean, app.Context) => Html)] =
Seq(
".md" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => markdown(fileContent, repository, enableWikiLink, enableRefsLink)(context)),
".md" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => markdown(fileContent, repository, enableWikiLink, enableRefsLink)(context)),
".markdown" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => markdown(fileContent, repository, enableWikiLink, enableRefsLink)(context))
)
@@ -47,9 +86,14 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
/**
* Converts Markdown of Wiki pages to HTML.
*/
def markdown(value: String, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink))
def markdown(value: String,
repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
enableRefsLink: Boolean,
enableTaskList: Boolean = false,
hasWritePermission: Boolean = false,
pages: List[String] = Nil)(implicit context: app.Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission, pages))
def renderMarkup(filePath: List[String], fileContent: String, branch: String,
repository: service.RepositoryService.RepositoryInfo,
@@ -105,12 +149,13 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
*/
def activityMessage(message: String)(implicit context: app.Context): Html =
Html(message
.replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/issues/$$3">$$1/$$2#$$3</a>""")
.replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/pull/$$3">$$1/$$2#$$3</a>""")
.replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1/$$2\">$$1/$$2</a>""")
.replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
.replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
.replaceAll("\\[user:([^\\s]+?)\\]" , (m: Match) => user(m.group(1)).body)
.replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/issues/$$3">$$1/$$2#$$3</a>""")
.replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/pull/$$3">$$1/$$2#$$3</a>""")
.replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1/$$2\">$$1/$$2</a>""")
.replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
.replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
.replaceAll("\\[user:([^\\s]+?)\\]" , (m: Match) => user(m.group(1)).body)
.replaceAll("\\[commit:([^\\s]+?)/([^\\s]+?)\\@([^\\s]+?)\\]", (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/commit/${m.group(3)}">${m.group(1)}/${m.group(2)}@${m.group(3).substring(0, 7)}</a>""")
)
/**
@@ -205,6 +250,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
}
}
def pre(value: Html): Html = Html(s"<pre>${value.body.trim.split("\n").map(_.trim).mkString("\n")}</pre>")
/**
* Implicit conversion to add mkHtml() to Seq[Html].
*/

View File

@@ -1,4 +1,5 @@
@(groupNames: List[String])(implicit context: app.Context)
@(groupNames: List[String],
isCreateRepoOptionPublic: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Create a New Repository"){
@@ -29,7 +30,7 @@
</fieldset>
<fieldset class="margin">
<label class="radio">
<input type="radio" name="isPrivate" value="false" checked>
<input type="radio" name="isPrivate" value="false" @if(isCreateRepoOptionPublic){checked}>
<span class="strong"><img src="@assets/common/images/repo_public.png"/>&nbsp;</i>&nbsp;Public</span><br>
<div>
<span>All users and guests can read this repository.</span>
@@ -38,7 +39,7 @@
</fieldset>
<fieldset>
<label class="radio">
<input type="radio" name="isPrivate" value="true">
<input type="radio" name="isPrivate" value="true" @if(!isCreateRepoOptionPublic){checked}>
<span class="strong"><img src="@assets/common/images/repo_private.png"/>&nbsp;</i>&nbsp;Private</span><br>
<div>
<span>Only collaborators can read this repository.</span>

View File

@@ -25,7 +25,7 @@
@if(repository.repository.description.isDefined){
<div>@repository.repository.description</div>
}
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
<div><span class="muted small">Updated @helper.html.datetimeago(repository.repository.lastActivityDate)</span></div>
</div>
</div>
}

View File

@@ -11,11 +11,6 @@
<li@if(active=="system"){ class="active"}>
<a href="@path/admin/system">System Settings</a>
</li>
@if(service.SystemSettingsService.enablePluginSystem){
<li@if(active=="plugins"){ class="active"}>
<a href="@path/admin/plugins">Plugins</a>
</li>
}
<li>
<a href="@path/console/login.jsp">H2 Console</a>
</li>

View File

@@ -1,37 +0,0 @@
@(plugins: List[app.SystemSettingsControllerBase.AvailablePlugin])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Plugins"){
@admin.html.menu("plugins"){
@tab("available")
<form action="@path/admin/plugins/_install" method="POST" validate="true">
<table class="table table-bordered">
<tr>
<th>ID</th>
<th>Version</th>
<th>Provider</th>
<th>Description</th>
</tr>
@plugins.zipWithIndex.map { case (plugin, i) =>
<tr>
<td>
<input type="checkbox" name="pluginId[@i]" value="@plugin.id"/>
@plugin.id
</td>
<td>@plugin.version</td>
<td><a href="@plugin.url">@plugin.author</a></td>
<td>@plugin.description</td>
</tr>
}
</table>
<input type="submit" id="install-plugins" class="btn btn-success" value="Install selected plugins"/>
</form>
}
}
<script>
$(function(){
$('#install-plugins').click(function(){
return confirm('Selected plugin will be installed. Are you sure?');
});
});
</script>

View File

@@ -1,37 +0,0 @@
@()(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("JavaScript Console"){
@admin.html.menu("plugins"){
@tab("console")
<form method="POST">
<div class="box">
<div class="box-header">JavaScript Console</div>
<div class="box-content">
<div id="editor" style="width: 100%; height: 400px;"></div>
</div>
</div>
<fieldset>
<input type="button" id="evaluate" class="btn btn-success" value="Evaluate"/>
</fieldset>
</form>
}
}
<script src="@assets/vendors/ace/ace.js" type="text/javascript" charset="utf-8"></script>
<script>
$(function(){
var editor = ace.edit("editor");
editor.setTheme("ace/theme/monokai");
editor.getSession().setMode("ace/mode/javascript");
$('#evaluate').click(function(){
$.post('@path/admin/plugins/console', {
script: editor.getValue()
}, function(data){
alert('Success: ' + data);
}).fail(function(error){
alert(error.statusText);
});
});
});
</script>

View File

@@ -1,47 +0,0 @@
@(plugins: List[plugin.Plugin],
updatablePlugins: List[app.SystemSettingsControllerBase.AvailablePlugin])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Plugins"){
@admin.html.menu("plugins"){
@tab("installed")
<form method="POST" validate="true">
<table class="table table-bordered">
<tr>
<th>ID</th>
<th>Version</th>
<th>Provider</th>
<th>Description</th>
</tr>
@plugins.zipWithIndex.map { case (plugin, i) =>
<tr>
<td>
<input type="checkbox" name="pluginId[@i]" value="@plugin.id"/>
@plugin.id
</td>
<td>
@plugin.version
@updatablePlugins.find(_.id == plugin.id).map { x =>
(@x.version is available)
}
</td>
<td><a href="@plugin.url">@plugin.author</a></td>
<td>@plugin.description</td>
</tr>
}
</table>
<input type="submit" id="update-plugins" class="btn btn-success" value="Update selected plugins" formaction="@path/admin/plugins/_update"/>
<input type="submit" id="delete-plugins" class="btn btn-danger" value="Uninstall selected plugins" formaction="@path/admin/plugins/_delete"/>
</form>
}
}
<script>
$(function(){
$('#update-plugins').click(function(){
return confirm('Selected plugin will be updated. Are you sure?');
});
$('#delete-plugins').click(function(){
return confirm('Selected plugin will be removed permanently. Are you sure?');
});
});
</script>

View File

@@ -1,9 +0,0 @@
@(active: String)(implicit context: app.Context)
@import context._
<ul class="nav nav-tabs">
<li@if(active == "installed"){ class="active"}><a href="@path/admin/plugins">Installed plugins</a></li>
<li@if(active == "available"){ class="active"}><a href="@path/admin/plugins/available">Available plugins</a></li>
@*
<li@if(active == "console" ){ class="active"}><a href="@path/admin/plugins/console">JavaScript console</a></li>
*@
</ul>

View File

@@ -31,6 +31,14 @@
You can use this property to adjust URL difference between the reverse proxy and GitBucket.
</p>
<!--====================================================================-->
<!-- Information -->
<!--====================================================================-->
<hr>
<label><span class="strong">Information</span> (HTML is available)</label>
<fieldset>
<textarea name="information" style="width: 600px; height: 100px;">@settings.information</textarea>
</fieldset>
<!--====================================================================-->
<!-- Account registration -->
<!--====================================================================-->
<hr>
@@ -45,6 +53,33 @@
<span class="strong">Deny</span> - Only administrators can create accounts.
</label>
</fieldset>
<hr>
<label class="strong">Default option to create a new repository</label>
<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.
</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.
</label>
</fieldset>
<!--====================================================================-->
<!-- Anonymous access -->
<!--====================================================================-->
<hr>
<label class="strong">Anonymous access</label>
<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.
</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
</label>
</fieldset>
<!--====================================================================-->
<!-- Services -->
<!--====================================================================-->
@@ -161,6 +196,13 @@
</label>
</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>
</div>
<div class="control-group">
<label class="control-label" for="ldapBindDN">Keystore</label>
<div class="controls">

View File

@@ -16,6 +16,9 @@
<input type="checkbox" name="removed" id="removed" value="true" @if(account.get.isRemoved){checked}/>
Disable
</label>
<div>
<span id="error-removed" class="error"></span>
</div>
}
</fieldset>
@if(account.map(_.password.nonEmpty).getOrElse(true)){

View File

@@ -0,0 +1,74 @@
@(openCount: Int,
closedCount: Int,
condition: service.IssuesService.IssueSearchCondition,
groups: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
<span class="small">
<a class="button-link@if(condition.state == "open"){ selected}" href="@condition.copy(state = "open").toURL">
<img src="@assets/common/images/status-open@(if(condition.state == "open"){"-active"}).png"/>
@openCount Open
</a>&nbsp;&nbsp;
<a class="button-link@if(condition.state == "closed"){ selected}" href="@condition.copy(state = "closed").toURL">
<img src="@assets/common/images/status-closed@(if(condition.state == "closed"){"-active"}).png"/>
@closedCount Closed
</a>
</span>
<div class="pull-right" id="table-issues-control">
@helper.html.dropdown("Visibility", flat = true){
<li>
<a href="@(condition.copy(visibility = (if(condition.visibility == Some("private")) None else Some("private"))).toURL)">
@helper.html.checkicon(condition.visibility == Some("private"))
Private repository only
</a>
</li>
<li>
<a href="@(condition.copy(visibility = (if(condition.visibility == Some("public")) None else Some("public"))).toURL)">
@helper.html.checkicon(condition.visibility == Some("public"))
Public repository only
</a>
</li>
}
@helper.html.dropdown("Organization", flat = true){
@groups.map { group =>
<li>
<a href="@((if(condition.groups.contains(group)) condition.copy(groups = condition.groups - group) else condition.copy(groups = condition.groups + group)).toURL)">
@helper.html.checkicon(condition.groups.contains(group))
@avatar(group, 20) @group
</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>

View File

@@ -1,50 +1,16 @@
@(listparts: play.twirl.api.Html,
allCount: Int,
assignedCount: Int,
createdByCount: Int,
repositories: List[(String, String, Int)],
@(issues: List[service.IssuesService.IssueInfo],
page: Int,
openCount: Int,
closedCount: Int,
condition: service.IssuesService.IssueSearchCondition,
filter: String)(implicit context: app.Context)
filter: String,
groups: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Your Issues"){
<div class="container">
@html.main("Issues"){
@dashboard.html.tab("issues")
<div class="row-fluid">
<div class="span3">
<ul class="nav nav-pills nav-stacked">
<li@if(filter == "all"){ class="active"}>
<a href="@path/dashboard/issues/repos@condition.toURL">
<span class="count-right">@allCount</span>
In your repositories
</a>
</li>
<li@if(filter == "assigned"){ class="active"}>
<a href="@path/dashboard/issues/assigned@condition.toURL">
<span class="count-right">@assignedCount</span>
Assigned to you
</a>
</li>
<li@if(filter == "created_by"){ class="active"}>
<a href="@path/dashboard/issues/created_by@condition.toURL">
<span class="count-right">@createdByCount</span>
Created by you
</a>
</li>
</ul>
<hr/>
<ul class="nav nav-pills nav-stacked small">
@repositories.map { case (owner, name, count) =>
<li@if(condition.repo == Some(owner + "/" + name)){ class="active"}>
<a href="@condition.copy(repo = Some(owner + "/" + name)).toURL">
<span class="count-right">@count</span>
@owner/@name
</a>
</li>
}
</ul>
</div>
@listparts
<div class="container">
@issuesnavi(filter, "issues", condition)
@issueslist(issues, page, openCount, closedCount, condition, filter, groups)
</div>
</div>
}

View File

@@ -3,182 +3,58 @@
openCount: Int,
closedCount: Int,
condition: service.IssuesService.IssueSearchCondition,
collaborators: List[String] = Nil,
milestones: List[model.Milestone] = Nil,
labels: List[model.Label] = Nil,
repository: Option[service.RepositoryService.RepositoryInfo] = None,
hasWritePermission: Boolean = false)(implicit context: app.Context)
filter: String,
groups: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@import service.IssuesService.IssueInfo
<div class="span9">
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL" id="clear-filter">
<i class="icon-remove-circle"></i> Clear milestone and label filters
</a>
}
@if(condition.repo.isDefined){
<a href="@condition.copy(repo = None).toURL" id="clear-filter">
<i class="icon-remove-circle"></i> Clear filter on @condition.repo
</a>
}
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 7, condition.toURL)
</div>
<div class="btn-group">
<a class="btn btn-small@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a>
<a class="btn btn-small@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a>
</div>
@helper.html.dropdown(
value = (condition.sort, condition.direction) match {
case ("created" , "desc") => "Newest"
case ("created" , "asc" ) => "Oldest"
case ("comments", "desc") => "Most commented"
case ("comments", "asc" ) => "Least commented"
case ("updated" , "desc") => "Recently updated"
case ("updated" , "asc" ) => "Least recently updated"
},
prefix = "Sort",
mini = false
){
<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>
}
<table class="table table-bordered table-hover table-issues">
@if(issues.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
No issues to show.
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL">Clear active filters.</a>
} else {
@if(repository.isDefined){
<a href="@url(repository.get)/issues/new">Create a new issue.</a>
}
}
</td>
</tr>
<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) =>
<tr>
<td style="padding-top: 15px; padding-bottom: 15px;">
@if(issue.isPullRequest){
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>
} else {
@if(hasWritePermission){
<tr>
<td style="background-color: #eee;">
<div class="btn-group">
<button class="btn btn-mini strong" id="state">@{if(condition.state == "open") "Close" else "Reopen"}</button>
</div>
@helper.html.dropdown("Label") {
@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>
@label.labelName
</a>
</li>
}
}
@helper.html.dropdown("Assignee") {
<li><a href="javascript:void(0);" class="toggle-assign" data-name=""><i class="icon-remove-circle"></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>
}
}
@helper.html.dropdown("Milestone") {
<li><a href="javascript:void(0);" class="toggle-milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
@milestones.map { milestone =>
<li>
<a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId">
<i class="icon-white"></i> @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert.png"/><span class="milestone-alert">Due by @date(dueDate)</span>
} else {
<span class="muted">Due by @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
</td>
</tr>
<img src="@assets/common/images/issue-@(if(issue.closed) "closed" else "open").png"/>
}
<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>
}
@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">
<img src="@assets/common/images/comment-active.png"> @commentCount
</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count" style="color: silver;">
<img src="@assets/common/images/comment.png"> @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(milestoneId = Some(Some(1))).toURL" class="username"><img src="@assets/common/images/milestone.png"> @milestone</a></span>
}
}
@issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
<tr>
<td>
@if(hasWritePermission){
<label class="checkbox" style="cursor: default;">
<input type="checkbox" value="@issue.issueId"/>
}
@if(issue.isPullRequest){
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>
} else {
<img src="@assets/common/images/issue-@(if(issue.closed) "closed" else "open").png"/>
}
@if(repository.isEmpty){
<a href="@path/@issue.userName/@issue.repositoryName">@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>
}
@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)
}
#@issue.issueId
</span>
<div class="small muted" style="margin-left: 20px;">
Opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)&nbsp;
@if(commentCount > 0){
<i class="icon-comment"></i><a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
}
</div>
@if(hasWritePermission){
</label>
}
</td>
</tr>
}
</table>
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
</div>
</div>
</td>
</tr>
}
</table>
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
</div>

View File

@@ -0,0 +1,22 @@
@(filter: String,
active: String,
condition: service.IssuesService.IssueSearchCondition)(implicit context: app.Context)
@import context._
@import view.helpers._
<ul class="nav nav-pills-group pull-left fill-width">
<li class="@if(filter == "created_by"){active} first">
<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">
<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>

View File

@@ -1,42 +1,16 @@
@(listparts: play.twirl.api.Html,
counts: List[service.PullRequestService.PullRequestCount],
repositories: List[(String, String, Int)],
@(issues: List[service.IssuesService.IssueInfo],
page: Int,
openCount: Int,
closedCount: Int,
condition: service.IssuesService.IssueSearchCondition,
filter: String)(implicit context: app.Context)
filter: String,
groups: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Your Issues"){
<div class="container">
@html.main("Pull Requests"){
@dashboard.html.tab("pulls")
<div class="row-fluid">
<div class="span3">
<ul class="nav nav-pills nav-stacked">
<li@if(filter == "created_by"){ class="active"}>
<a href="@path/dashboard/pulls/owned@condition.toURL">
<span class="count-right">@counts.find(_.userName == loginAccount.get.userName).map(_.count).getOrElse(0)</span>
Yours
</a>
</li>
<li@if(filter == "not_created_by"){ class="active"}>
<a href="@path/dashboard/pulls/public@condition.toURL">
<span class="count-right">@counts.filter(_.userName != loginAccount.get.userName).map(_.count).sum</span>
Public
</a>
</li>
</ul>
<hr/>
<ul class="nav nav-pills nav-stacked small">
@repositories.map { case (owner, name, count) =>
<li@if(condition.repo == Some(owner + "/" + name)){ class="active"}>
<a href="@path/dashboard/pulls/for/@owner/@name">
<span class="count-right">@count</span>
@owner/@name
</a>
</li>
}
</ul>
</div>
@listparts
<div class="container">
@issuesnavi(filter, "pulls", condition)
@issueslist(issues, page, openCount, closedCount, condition, filter, groups)
</div>
</div>
}

View File

@@ -1,101 +0,0 @@
@(issues: List[service.IssuesService.IssueInfo],
page: Int,
openCount: Int,
closedCount: Int,
condition: service.IssuesService.IssueSearchCondition,
repository: Option[service.RepositoryService.RepositoryInfo],
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@import service.IssuesService.IssueInfo
<div class="span9">
@repository.map { repository =>
@if(hasWritePermission){
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.PullRequestService.PullRequestLimit, 7, condition.toURL)
<a href="@url(repository)/compare" class="btn btn-small btn-success">New pull request</a>
</div>
}
}
<div class="btn-group">
<a class="btn btn-small@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a>
<a class="btn btn-small@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a>
</div>
@helper.html.dropdown(
value = (condition.sort, condition.direction) match {
case ("created" , "desc") => "Newest"
case ("created" , "asc" ) => "Oldest"
case ("comments", "desc") => "Most commented"
case ("comments", "asc" ) => "Least commented"
case ("updated" , "desc") => "Recently updated"
case ("updated" , "asc" ) => "Least recently updated"
},
prefix = "Sort",
mini = false
){
<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>
}
<table class="table table-bordered table-hover table-issues">
@if(issues.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
No pull requests to show.
</td>
</tr>
}
@issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
<tr>
<td>
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
<span class="pull-right muted">#@issue.issueId</span>
<div style="margin-left: 20px;">
@issue.content.map { content =>
@cut(content, 90)
}.getOrElse {
<span class="muted">No description available</span>
}
</div>
<div class="small muted" style="margin-left: 20px;">
@avatarLink(issue.openedUserName, 20) by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)&nbsp;
@if(commentCount > 0){
<i class="icon-comment"></i><a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
}
</div>
</td>
</tr>
}
</table>
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.PullRequestService.PullRequestLimit, 10, condition.toURL)
</div>
</div>

View File

@@ -1,13 +1,47 @@
@(active: String = "")(implicit context: app.Context)
@import context._
@import view.helpers._
<ul class="nav nav-tabs">
<li@if(active == ""){ class="active"}><a href="@path/">News Feed</a></li>
@if(loginAccount.isDefined){
<li@if(active == "pulls" ){ class="active"}><a href="@path/dashboard/pulls">Pull Requests</a></li>
<li@if(active == "issues"){ class="active"}><a href="@path/dashboard/issues/repos">Issues</a></li>
}
@if(active == ""){
<li class="pull-right"><a href="@path/activities.atom"><img src="@assets/common/images/feed.png" alt="activities"></a></li>
}
</ul>
<div class="dashboard-nav">
<div class="container">
<a href="@path/" @if(active == ""){ class="active"}>
<img src="@assets/common/images/menu-feed.png">
News Feed
</a>
@if(loginAccount.isDefined){
<a href="@path/dashboard/pulls" @if(active == "pulls" ){ class="active"}>
<img src="@assets/common/images/menu-pulls.png">
Pull Requests
</a>
<a href="@path/dashboard/issues" @if(active == "issues"){ class="active"}>
<img src="@assets/common/images/menu-issues.png">
Issues
</a>
}
</div>
</div>
<style type="text/css">
div.dashboard-nav {
border-bottom: 1px solid #ddd;
text-align: right;
height: 32px;
margin-bottom: 20px;
}
div.dashboard-nav a {
line-height: 10px;
margin-left: 20px;
padding-bottom: 13px;
padding-left: 4px;
padding-right: 4px;
color: #888;
}
div.dashboard-nav a:hover {
text-decoration: none;
}
div.dashboard-nav a.active {
border-bottom: 2px solid #bb4444;
color: #333;
}
</style>

View File

@@ -10,6 +10,7 @@
@(activity.activityType match {
case "open_issue" => detailActivity(activity, "activity-issue.png")
case "comment_issue" => detailActivity(activity, "activity-comment.png")
case "comment_commit" => detailActivity(activity, "activity-comment.png")
case "close_issue" => detailActivity(activity, "activity-issue-close.png")
case "reopen_issue" => detailActivity(activity, "activity-issue-reopen.png")
case "open_pullreq" => detailActivity(activity, "activity-merge.png")
@@ -62,7 +63,7 @@
@detailActivity(activity: model.Activity, image: String) = {
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
<div class="activity-content">
<div class="muted small">@datetime(activity.activityDate)</div>
<div class="muted small">@helper.html.datetimeago(activity.activityDate)</div>
<div class="strong">
@avatar(activity.activityUserName, 16)
@activityMessage(activity.message)
@@ -76,7 +77,7 @@
@customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = {
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
<div class="activity-content">
<div class="muted small">@datetime(activity.activityDate)</div>
<div class="muted small">@helper.html.datetimeago(activity.activityDate)</div>
<div class="strong">
@avatar(activity.activityUserName, 16)
@activityMessage(activity.message)
@@ -91,7 +92,7 @@
<div>
@avatar(activity.activityUserName, 16)
@activityMessage(activity.message)
<span class="muted small">@datetime(activity.activityDate)</span>
<span class="muted small">@helper.html.datetimeago(activity.activityDate)</span>
</div>
</div>
}

View File

@@ -7,18 +7,24 @@
@defining("(id=\")([\\w\\-]*)(\")".r.findFirstMatchIn(textarea.body).map(_.group(2))){ textareaId =>
<script>
$(function(){
$([$('#@textareaId').closest('div')[0], $('#@textareaId').next('div')[0]]).dropzone({
url: '@path/upload/image/@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>",
success: function(file, id) {
var images = '\n![' + file.name.split('.')[0] + '](@baseUrl/@owner/@repository/_attached/' + id + ')';
$('#@textareaId').val($('#@textareaId').val() + images);
$(file.previewElement).prevAll('div.dz-preview').addBack().remove();
try {
$([$('#@textareaId').closest('div')[0], $('#@textareaId').next('div')[0]]).dropzone({
url: '@path/upload/image/@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>",
success: function(file, id) {
var images = '\n![' + file.name.split('.')[0] + '](@baseUrl/@owner/@repository/_attached/' + id + ')';
$('#@textareaId').val($('#@textareaId').val() + images);
$(file.previewElement).prevAll('div.dz-preview').addBack().remove();
}
});
} catch(e) {
if (e.message !== "Dropzone already attached.") {
throw e;
}
});
}
// Adjust clickable area width
$('#@textareaId').next('div.clickable').css('width', ($('#@textareaId').width() + 8) + 'px');

View File

@@ -0,0 +1,62 @@
@(branch: String = "",
repository: service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(body: Html)(implicit context: app.Context)
@import context._
@import 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 = true
) {
<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>
@body
@if(hasWritePermission) {
<li id="create-branch" style="display: none;">
<a><form action="@url(repository)/branches" method="post" style="margin: 0;">
<span class="new-branch-name">Create branch:&nbsp;<span class="new-branch"></span></span>
<br><span style="padding-left: 17px;">from&nbsp;'@branch'</span>
<input type="hidden" name="new">
<input type="hidden" name="from" value="@branch">
</form></a>
</li>
}
}
<script>
$(function(){
$('#branch-control-input').parent().click(function(e) {
e.stopPropagation();
});
$('#branch-control-close').click(function() {
$('[data-toggle="dropdown"]').parent().removeClass('open');
});
$('#branch-control-input').keyup(function() {
var inputVal = $('#branch-control-input').val();
$.each($('#branch-control-input').parent().parent().find('a'), function(index, elem) {
if (!inputVal || !elem.text.trim() || elem.text.trim().lastIndexOf(inputVal, 0) >= 0) {
$(elem).parent().show();
} else {
$(elem).parent().hide();
}
});
@if(hasWritePermission) {
if (inputVal) {
$('#create-branch').parent().find('li:last-child').show().find('.new-branch').text(inputVal);
} else {
$('#create-branch').parent().find('li:last-child').hide();
}
}
});
@if(hasWritePermission) {
$('#create-branch').click(function() {
$(this).find('input[name="new"]').val($('.dropdown-menu input').val())
$(this).find('form').submit()
});
}
$('.btn-group').click(function() {
$('#branch-control-input').val('');
$('.dropdown-menu li').show();
$('#create-branch').hide();
});
});
</script>

View File

@@ -0,0 +1,35 @@
@(comment: model.CommitComment,
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo,
latestCommitId: Option[String] = None)(implicit context: app.Context)
@import context._
@import view.helpers._
<div class="@if(comment.fileName.isDefined && (!latestCommitId.isDefined || latestCommitId.get == comment.commitId)){inline-comment}" @if(comment.fileName.isDefined){filename=@comment.fileName.get} @if(comment.newLine.isDefined){newline=@comment.newLine.get} @if(comment.oldLine.isDefined){oldline=@comment.oldLine.get}>
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
<div class="box commit-comment-box commit-comment-@comment.commentId">
<div class="box-header-small">
@user(comment.commentedUserName, styleClass="username strong")
<span class="muted">
commented
@if(comment.pullRequest){
on this Pull Request
}else{
@if(comment.fileName.isDefined){
on @comment.fileName.get
}
in <a href="@path/@repository.owner/@repository.name/commit/@comment.commitId">@comment.commitId.substring(0, 7)</a>
}
@helper.html.datetimeago(comment.registeredDate)
</span>
<span class="pull-right">
@if(hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false)){
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>&nbsp;
<a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle"></i></a>
}
</span>
</div>
<div class="box-content commit-commentContent-@comment.commentId">
@markdown(comment.content, repository, false, true, true, hasWritePermission)
</div>
</div>
</div>

View File

@@ -0,0 +1,10 @@
@(latestUpdatedDate: java.util.Date,
recentOnly: Boolean = true)
@import view.helpers._
<span data-toggle="tooltip" title="@datetime(latestUpdatedDate)">
@if(recentOnly){
@datetimeAgoRecentOnly(latestUpdatedDate)
}else{
@datetimeAgo(latestUpdatedDate)
}
</span>

View File

@@ -1,8 +1,11 @@
@(diffs: Seq[util.JGitUtil.DiffInfo],
repository: service.RepositoryService.RepositoryInfo,
newCommitId: Option[String],
oldCommitId: Option[String],
showIndex: Boolean)(implicit context: app.Context)
repository: service.RepositoryService.RepositoryInfo,
newCommitId: Option[String],
oldCommitId: Option[String],
showIndex: Boolean,
issueId: Option[Int],
hasWritePermission: Boolean,
showLineNotes: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
@@ -39,29 +42,36 @@
}
@diffs.zipWithIndex.map { case (diff, i) =>
<a name="diff-@i"></a>
<table class="table table-bordered">
<table class="table table-bordered" commitId="@newCommitId" fileName="@diff.newPath">
<tr>
<th style="font-weight: normal; line-height: 27px;" class="box-header">
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
@diff.oldPath -> @diff.newPath
<img src="@assets/common/images/diff_move.png"/> @diff.oldPath -> @diff.newPath
@if(newCommitId.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/@newCommitId.get/@diff.newPath" class="btn btn-small">View file @@ @newCommitId.get.substring(0, 10)</a>
</div>
}
}
@if(diff.changeType == ChangeType.ADD || diff.changeType == ChangeType.MODIFY){
@diff.newPath
@if(diff.changeType == ChangeType.ADD){
<img src="@assets/common/images/diff_add.png"/>
}else{
<img src="@assets/common/images/diff_edit.png"/>
} @diff.newPath
@if(newCommitId.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/@newCommitId.get/@diff.newPath" class="btn btn-small">View file @@ @newCommitId.get.substring(0, 10)</a>
</div>
}
}
@if(diff.changeType == ChangeType.DELETE){
@diff.oldPath
<img src="@assets/common/images/diff_delete.png"/> @diff.oldPath
@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">View file @@ @oldCommitId.get.substring(0, 10)</a>
</div>
}
@@ -110,7 +120,15 @@ $(function(){
renderDiffs(0);
});
$('.toggle-notes').change(function() {
if (!$(this).prop('checked')) {
$(this).closest('table').find('.not-diff.inline-comment-form').remove();
}
$(this).closest('table').find('.not-diff').toggle();
});
function renderDiffs(viewType){
window.viewType = viewType;
@diffs.zipWithIndex.map { case (diff, i) =>
@if(diff.newContent != None || diff.oldContent != None){
if($('#oldText-@i').length > 0){
@@ -118,6 +136,116 @@ $(function(){
}
}
}
@if(showLineNotes){
function getInlineContainer(where) {
if (viewType == 0) {
if (where === 'new') {
return $('<tr class="not-diff"><td colspan="2"></td><td colspan="2" class="comment-box-container"></td></tr>');
} else if (where === 'old') {
return $('<tr class="not-diff"><td colspan="2" class="comment-box-container"></td><td colspan="2"></td></tr>');
}
}
return $('<tr class="not-diff"><td colspan="3" class="comment-box-container"></td></tr>');
}
$('.inline-comment').each(function(i, v) {
var $v = $(v), filename = $v.attr('filename'),
oldline = $v.attr('oldline'), newline = $v.attr('newline');
if (typeof $('#show-notes')[0] !== 'undefined' && !$('#show-notes')[0].checked) {
$(this).hide();
}
var tmp;
var diff;
if (typeof oldline !== 'undefined') {
if (typeof newline !== 'undefined') {
tmp = getInlineContainer();
} else {
tmp = getInlineContainer('old');
}
tmp.children('td:first').html($(this).clone().show());
diff = $('table[filename="' + filename + '"]');
diff.find('table.diff').find('.oldline[line-number=' + oldline + ']')
.parent().nextAll(':not(.not-diff):first').before(tmp);
} else {
tmp = getInlineContainer('new');
tmp.children('td:last').html($(this).clone().show());
diff = $('table[filename="' + filename + '"]');
diff.find('table.diff').find('.newline[line-number=' + newline + ']')
.parent().nextAll(':not(.not-diff):first').before(tmp);
}
if (!diff.find('.toggle-notes').prop('checked')) {
tmp.hide();
}
});
@if(hasWritePermission) {
$('table.diff td').hover(
function() {
$(this).find('b').css('display', 'inline-block');
},
function() {
$(this).find('b').css('display', 'none');
}
);
$('table.diff th').hover(
function() {
$(this).nextAll().find('b').first().css('display', 'inline-block');
},
function() {
$(this).nextAll().find('b').first().css('display', 'none');
}
);
$('.add-comment').click(function() {
var $this = $(this),
$tr = $this.closest('tr'),
$check = $this.closest('table:not(.diff)').find('.toggle-notes');
if (!$check.prop('checked')) {
$check.prop('checked', true).trigger('change');
}
if (!$tr.nextAll(':not(.not-diff):first').prev().hasClass('inline-comment-form')) {
var commitId = $this.closest('.table-bordered').attr('commitId'),
fileName = $this.closest('.table-bordered').attr('fileName'),
oldLineNumber, newLineNumber,
url = '@url(repository)/commit/' + commitId + '/comment/_form?fileName=' + fileName@issueId.map { id => + '&issueId=@id' };
if (viewType == 0) {
oldLineNumber = $this.parent().prev('.oldline').attr('line-number');
newLineNumber = $this.parent().prev('.newline').attr('line-number');
} else {
oldLineNumber = $this.parent().prevAll('.oldline').attr('line-number');
newLineNumber = $this.parent().prevAll('.newline').attr('line-number');
}
if (!isNaN(oldLineNumber) && oldLineNumber) {
url += ('&oldLineNumber=' + oldLineNumber)
}
if (!isNaN(newLineNumber) && newLineNumber) {
url += ('&newLineNumber=' + newLineNumber)
}
$.get(
url,
{
dataType : 'html'
},
function(responseContent) {
$this.hide();
var tmp;
if (!isNaN(oldLineNumber) && oldLineNumber) {
if (!isNaN(newLineNumber) && newLineNumber) {
tmp = getInlineContainer();
} else {
tmp = getInlineContainer('old');
}
} else {
tmp = getInlineContainer('new');
}
tmp.addClass('inline-comment-form').children('.comment-box-container').html(responseContent);
$tr.nextAll(':not(.not-diff):first').before(tmp);
}
);
}
});
$('table.diff').on('click', '.btn-default', function() {
$(this).closest('.inline-comment-form').remove();
});
}
}
}
});
</script>
</script>

View File

@@ -0,0 +1,7 @@
@(error: Option[Any])
@if(error.isDefined){
<div class='alert alert-danger'>
<button type="button" class="close" data-dismiss="alert">&times;</button>
@error
</div>
}

View File

@@ -0,0 +1,18 @@
@(repository: service.RepositoryService.RepositoryInfo,
groupAndPerm: Map[String, Boolean])(implicit context: app.Context)
@import context._
@import 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>
@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>
} 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>
<input id="account" name="account" type="hidden" />
</form>

View File

@@ -1,7 +1,7 @@
@(info: Option[Any])
@if(info.isDefined){
<div class="alert alert-info">
<button type="button" class="close" data-dismiss="alert">×</button>
<button type="button" class="close" data-dismiss="alert">&times;</button>
@info
</div>
}

View File

@@ -1,17 +1,17 @@
@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean,
style: String = "", placeholder: String = "Leave a comment", elastic: Boolean = false)(implicit context: app.Context)
@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean, hasWritePermission: Boolean,
style: String = "", placeholder: String = "Leave a comment", elastic: Boolean = false, uid: Long = new java.util.Date().getTime())(implicit context: app.Context)
@import context._
@import view.helpers._
<div class="tabbable">
<ul class="nav nav-tabs" style="height: 37px;">
<li class="active"><a href="#tab1" data-toggle="tab">Write</a></li>
<li><a href="#tab2" data-toggle="tab" id="preview">Preview</a></li>
<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="tab1">
<div class="tab-pane active" id="tab@uid">
<span id="error-content" class="error"></span>
@textarea = {
<textarea id="content" name="content"@if(style.nonEmpty){ style="@style"} placeholder="@placeholder">@content</textarea>
<textarea id="content@uid" name="content"@if(style.nonEmpty){ style="@style"} placeholder="@placeholder">@content</textarea>
}
@if(enableWikiLink){
@textarea
@@ -19,8 +19,8 @@
@helper.html.attached(repository.owner, repository.name)(textarea)
}
</div>
<div class="tab-pane" id="tab2">
<div class="markdown-body" id="preview-area">
<div class="tab-pane" id="tab@(uid+1)">
<div class="markdown-body" id="preview-area@uid">
</div>
</div>
</div>
@@ -30,17 +30,18 @@
<script>
$(function(){
@if(elastic){
$('#content').elastic();
$('#content@uid').elastic();
}
$('#preview').click(function(){
$('#preview-area').html('<img src="@assets/common/images/indicator.gif"> Previewing...');
$('#preview@uid').click(function(){
$('#preview-area@uid').html('<img src="@assets/common/images/indicator.gif"> Previewing...');
$.post('@url(repository)/_preview', {
content : $('#content').val(),
content : $('#content@uid').val(),
enableWikiLink : @enableWikiLink,
enableRefsLink : @enableRefsLink
enableRefsLink : @enableRefsLink,
enableTaskList : @enableTaskList
}, function(data){
$('#preview-area').html(data);
$('#preview-area@uid').html(data);
prettyPrint();
});
});

View File

@@ -4,13 +4,23 @@
@import context._
@import view.helpers._
@main("GitBucket"){
@dashboard.html.tab()
<div class="container">
@dashboard.html.tab()
<div class="row-fluid">
<div class="span8">
<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">
@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>
@Html(information)
</div>
}
@if(loginAccount.isEmpty){
@signinform(settings)
} else {

View File

@@ -10,7 +10,7 @@
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
<div class="box issue-comment-box">
<div class="box-content">
@helper.html.preview(repository, "", false, true, "width: 635px; height: 100px; max-height: 150px;", elastic = true)
@helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 635px; height: 100px; max-height: 150px;", elastic = true)
</div>
</div>
<div class="pull-right">
@@ -28,4 +28,4 @@ $(function(){
$('<input type="hidden">').attr('name', 'action').val($(this).val().toLowerCase()).appendTo('form');
});
});
</script>
</script>

View File

@@ -1,107 +1,115 @@
@(issue: model.Issue,
comments: List[model.IssueComment],
@(issue: Option[model.Issue],
comments: List[model.Comment],
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo,
pullreq: Option[model.PullRequest] = None)(implicit context: app.Context)
@import context._
@import view.helpers._
<div class="issue-avatar-image">@avatar(issue.openedUserName, 48)</div>
<div class="box issue-comment-box">
<div class="box-header-small">
@user(issue.openedUserName, styleClass="username strong") <span class="muted">commented on @datetime(issue.registeredDate)</span>
<span class="pull-right">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a href="#" data-issue-id="@issue.issueId"><i class="icon-pencil"></i></a>
}
</span>
@if(issue.isDefined){
<div class="issue-avatar-image">@avatar(issue.get.openedUserName, 48)</div>
<div class="box issue-comment-box">
<div class="box-header-small">
@user(issue.get.openedUserName, styleClass="username strong") <span class="muted">commented @helper.html.datetimeago(issue.get.registeredDate)</span>
<span class="pull-right">
@if(hasWritePermission || loginAccount.map(_.userName == issue.get.openedUserName).getOrElse(false)){
<a href="#" data-issue-id="@issue.get.issueId"><i class="icon-pencil"></i></a>
}
</span>
</div>
<div class="box-content issue-content" id="issueContent">
@markdown(issue.get.content getOrElse "No description provided.", repository, false, true, true, hasWritePermission)
</div>
</div>
<div class="box-content issue-content" id="issueContent">
@markdown(issue.content getOrElse "No description provided.", repository, false, true)
</div>
</div>
}
@comments.map { comment =>
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
<div class="box issue-comment-box" id="comment-@comment.commentId">
<div class="box-header-small">
@user(comment.commentedUserName, styleClass="username strong")
<span class="muted">
@if(comment.action == "comment"){
commented
} else {
@if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request }
}
on @datetime(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))){
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>&nbsp;
<a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle"></i></a>
}
</span>
</div>
<div class="box-content"class="issue-content" id="commentContent-@comment.commentId">
@if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){
@defining(comment.content.substring(comment.content.length - 40)){ id =>
<div class="pull-right"><a href="@path/@repository.owner/@repository.name/commit/@id" class="monospace">@id.substring(0, 7)</a></div>
@markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true)
}
} 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>
@comments.map {
case comment: 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-small">
@user(comment.commentedUserName, styleClass="username strong")
<span class="muted">
@if(comment.action == "comment"){
commented
} else {
@if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request }
}
@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))){
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>&nbsp;
<a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle"></i></a>
}
</span>
</div>
<div class="box-content"class="issue-content" id="commentContent-@comment.commentId">
@if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){
@defining(comment.content.substring(comment.content.length - 40)){ id =>
<div class="pull-right"><a href="@path/@repository.owner/@repository.name/commit/@id" class="monospace">@id.substring(0, 7)</a></div>
@markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true, true, hasWritePermission)
}
} else {
@markdown(comment.content, repository, false, true)
@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>
}
} else {
@markdown(comment.content, repository, false, true, true, hasWritePermission)
}
}
</div>
</div>
}
@if(comment.action == "merge"){
<div class="small" style="margin-top: 10px; margin-bottom: 10px;">
<span class="label label-info">Merged</span>
@avatar(comment.commentedUserName, 20)
@user(comment.commentedUserName, styleClass="username strong") merged commit <code>@pullreq.map(_.commitIdTo.substring(0, 7))</code> into
@if(pullreq.get.requestUserName == repository.owner){
<span class="label label-info monospace">@pullreq.map(_.branch)</span> from <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span>
} else {
<span class="label label-info monospace">@pullreq.map(_.userName):@pullreq.map(_.branch)</span> from <span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span>
}
@helper.html.datetimeago(comment.registeredDate)
</div>
}
@if(comment.action == "close" || comment.action == "close_comment"){
<div class="small issue-comment-action">
<span class="label label-important">Closed</span>
@avatar(comment.commentedUserName, 20)
@if(issue.isDefined && issue.get.isPullRequest){
@user(comment.commentedUserName, styleClass="username strong") closed the pull request @helper.html.datetimeago(comment.registeredDate)
} else {
@user(comment.commentedUserName, styleClass="username strong") closed the issue @helper.html.datetimeago(comment.registeredDate)
}
</div>
</div>
}
@if(comment.action == "reopen" || comment.action == "reopen_comment"){
<div class="small issue-comment-action">
<span class="label label-success">Reopened</span>
@avatar(comment.commentedUserName, 20)
@user(comment.commentedUserName, styleClass="username strong") reopened the issue @helper.html.datetimeago(comment.registeredDate)
</div>
}
@if(comment.action == "delete_branch"){
<div class="small issue-comment-action">
<span class="label">Deleted</span>
@avatar(comment.commentedUserName, 20)
@user(comment.commentedUserName, styleClass="username strong") deleted the <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> branch @helper.html.datetimeago(comment.registeredDate)
</div>
}
}
@if(comment.action == "merge"){
<div class="small" style="margin-top: 10px; margin-bottom: 10px;">
<span class="label label-info">Merged</span>
@avatar(comment.commentedUserName, 20)
@user(comment.commentedUserName, styleClass="username strong") merged commit <code>@pullreq.map(_.commitIdTo.substring(0, 7))</code> into
@if(pullreq.get.requestUserName == repository.owner){
<span class="label label-info monospace">@pullreq.map(_.branch)</span> from <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span>
} else {
<span class="label label-info monospace">@pullreq.map(_.userName):@pullreq.map(_.branch)</span> to <span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span>
}
@datetime(comment.registeredDate)
</div>
}
@if(comment.action == "close" || comment.action == "close_comment"){
<div class="small issue-comment-action">
<span class="label label-important">Closed</span>
@avatar(comment.commentedUserName, 20)
@if(issue.isPullRequest){
@user(comment.commentedUserName, styleClass="username strong") closed the pull request @datetime(comment.registeredDate)
} else {
@user(comment.commentedUserName, styleClass="username strong") closed the issue @datetime(comment.registeredDate)
}
</div>
}
@if(comment.action == "reopen" || comment.action == "reopen_comment"){
<div class="small issue-comment-action">
<span class="label label-success">Reopened</span>
@avatar(comment.commentedUserName, 20)
@user(comment.commentedUserName, styleClass="username strong") reopened the issue @datetime(comment.registeredDate)
</div>
}
@if(comment.action == "delete_branch"){
<div class="small issue-comment-action">
<span class="label">Deleted</span>
@avatar(comment.commentedUserName, 20)
@user(comment.commentedUserName, styleClass="username strong") deleted the <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> branch @datetime(comment.registeredDate)
</div>
case comment: model.CommitComment => {
@helper.html.commitcomment(comment, hasWritePermission, repository, pullreq.map(_.commitIdTo))
}
}
<script>
$(function(){
$('i.icon-pencil').click(function(){
@if(issue.isDefined){
$('.issue-comment-box i.icon-pencil').click(function(){
var id = $(this).closest('a').data('comment-id');
var url = '@url(repository)/issue_comments/_data/' + id;
var $content = $('#commentContent-' + id);
@@ -134,5 +142,122 @@ $(function(){
}
return false;
});
}
$(document).on('click', '.commit-comment-box i.icon-pencil', function(){
var id = $(this).closest('a').data('comment-id');
var url = '@url(repository)/commit_comments/_data/' + id;
var $content = $('.commit-commentContent-' + id, $(this).closest('.box'));
$.get(url,
{
dataType : 'html'
},
function(data){
$content.empty().html(data);
});
return false;
});
$(document).on('click', '.commit-comment-box i.icon-remove-circle', function(){
if(confirm('Are you sure you want to delete this?')) {
var id = $(this).closest('a').data('comment-id');
$.post('@url(repository)/commit_comments/delete/' + id,
function(data){
if(data > 0) {
$('.commit-comment-' + id).closest('.not-diff').remove();
$('.commit-comment-' + id).closest('.inline-comment').remove();
}
});
}
return false;
});
var extractMarkdown = function(data){
$('body').append('<div id="tmp"></div>');
$('#tmp').html(data);
var markdown = $('#tmp textarea').val();
$('#tmp').remove();
return markdown;
};
var replaceTaskList = function(issueContentHtml, checkboxes) {
var ss = [],
markdown = extractMarkdown(issueContentHtml),
xs = markdown.split(/- \[[x| ]\]/g);
for (var i=0; i<xs.length; i++) {
ss.push(xs[i]);
if (checkboxes.eq(i).prop('checked')) ss.push('- [x]');
else ss.push('- [ ]');
}
ss.pop();
return ss.join('');
};
$('div[class*=commit-commentContent-]').on('click', ':checkbox', function(ev){
var $commentContent = $(ev.target).parents('div[class*=commit-commentContent-]'),
commentId = $commentContent.attr('class').match(/commit-commentContent-.+/)[0].replace(/commit-commentContent-/, ''),
checkboxes = $commentContent.find(':checkbox');
$.get('@url(repository)/commit_comments/_data/' + commentId,
{
dataType : 'html'
},
function(responseContent){
$.ajax({
url: '@url(repository)/commit_comments/edit/' + commentId,
type: 'POST',
data: {
issueId : 0,
content : replaceTaskList(responseContent, checkboxes)
},
success: function(data) {
$('.commit-commentContent-' + commentId).html(data.content);
}
});
}
);
});
@if(issue.isDefined){
$('#issueContent').on('click', ':checkbox', function(ev){
var checkboxes = $('#issueContent :checkbox');
$.get('@url(repository)/issues/_data/@issue.get.issueId',
{
dataType : 'html'
},
function(responseContent){
$.ajax({
url: '@url(repository)/issues/edit/@issue.get.issueId',
type: 'POST',
data: {
title : $('#issueTitle').text(),
content : replaceTaskList(responseContent, checkboxes)
}
});
}
);
});
$('div[id^=commentContent-]').on('click', ':checkbox', function(ev){
var $commentContent = $(ev.target).parents('div[id^=commentContent-]'),
commentId = $commentContent.attr('id').replace(/commentContent-/, ''),
checkboxes = $commentContent.find(':checkbox');
$.get('@url(repository)/issue_comments/_data/' + commentId,
{
dataType : 'html'
},
function(responseContent){
$.ajax({
url: '@url(repository)/issue_comments/edit/' + commentId,
type: 'POST',
data: {
issueId : 0,
content : replaceTaskList(responseContent, checkboxes)
}
});
}
);
});
}
});
</script>
</script>

View File

@@ -7,7 +7,7 @@
@import view.helpers._
@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("issues", repository){
@tab("issues", false, 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">
@@ -57,7 +57,7 @@
</div>
</div>
<hr>
@helper.html.preview(repository, "", false, true, "width: 565px; height: 200px; max-height: 250px;", elastic = true)
@helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 565px; height: 200px; max-height: 250px;", elastic = true)
</div>
</div>
<div class="pull-right">

View File

@@ -10,47 +10,44 @@
@import view.helpers._
@html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("issues", repository){
<ul class="nav nav-tabs pull-left fill-width">
<li class="pull-left">
<h1>
<span class="show-title">
<span id="show-title">@issue.title</span>
<span class="muted">#@issue.issueId</span>
</span>
<span class="edit-title" style="display: none;">
<span id="error-edit-title" class="error"></span>
<input type="text" class="span9" id="edit-title" value="@issue.title"/>
</span>
</h1>
@if(issue.closed) {
<span class="label label-important issue-status">Closed</span>
} else {
<span class="label label-success issue-status">Open</span>
<div>
<div class="show-title pull-right">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a class="btn btn-small" href="#" id="edit">Edit</a>
}
<span class="muted">
@user(issue.openedUserName, styleClass="username strong") opened this issue on @datetime(issue.registeredDate) - @defining(
comments.filter( _.action.contains("comment") ).size
){ count =>
@count @plural(count, "comment")
}
<a class="btn btn-small 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>
</div>
<h1>
<span class="show-title">
<span id="show-title">@issue.title</span>
<span class="muted">#@issue.issueId</span>
</span>
<br/><br/>
</li>
<li class="pull-right">
<div class="show-title">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a class="btn btn-small" href="#" id="edit">Edit</a>
}
<a class="btn btn-small btn-success" href="@url(repository)/issues/new">New issue</a>
</div>
<div class="edit-title" style="display: none;">
<a class="btn" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a>
</div>
</li>
</ul>
<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"/>
</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")
}
</span>
<br/><br/>
<hr>
<div class="row-fluid">
<div class="span10">
@commentlist(issue, comments, hasWritePermission, repository)
@commentlist(Some(issue), comments, hasWritePermission, repository)
@commentform(issue, true, hasWritePermission, repository)
</div>
<div class="span2">
@@ -92,4 +89,4 @@ $(function(){
return false;
});
});
</script>
</script>

View File

@@ -1,5 +1,5 @@
@(issue: model.Issue,
comments: List[model.IssueComment],
comments: List[model.Comment],
issueLabels: List[model.Label],
collaborators: List[String],
milestones: List[(model.Milestone, Int, Int)],
@@ -33,7 +33,7 @@
<span class="muted small strong">Milestone</span>
@if(hasWritePermission){
<div class="pull-right">
@helper.html.dropdown() {
@helper.html.dropdown(right = true) {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
@milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
<li>
@@ -69,14 +69,16 @@
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
<span class="strong small">@milestone.title</span>
}
}.getOrElse(<span class="muted small">No milestone</span>)
}.getOrElse {
<span class="muted small">No milestone</span>
}
</span>
<hr/>
<div style="margin-bottom: 8px;">
<span class="muted small strong">Assignee</span>
@if(hasWritePermission){
<div class="pull-right">
@helper.html.dropdown() {
@helper.html.dropdown(right = true) {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
@collaborators.map { collaborator =>
<li>
@@ -92,7 +94,9 @@
<span id="label-assigned">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20) @user(userName, styleClass="username strong small")
}.getOrElse(<span class="muted small">No one</span>)
}.getOrElse{
<span class="muted small">No one</span>
}
</span>
<hr/>
<div style="margin-bottom: 8px;">

View File

@@ -6,14 +6,14 @@
<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)" readonly style="width: 100px;">
<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>
</div>
<script>
$('div#label-color-@labelId').colorpicker();
</script>
<span id="label-error-@labelId" class="error" style="padding-left: 40px;"></span>
<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="submit-@labelId" class="btn btn-success" style="margin-bottom: 0px;" value="@(if(labelId == "new") "Create label" else "Save changes")"/>
</span>

View File

@@ -6,7 +6,8 @@
@import view.helpers._
@html.main(s"Labels - ${repository.owner}/${repository.name}"){
@html.menu("issues", repository){
@issues.html.tab("labels", hasWritePermission, repository)
@issues.html.navigation("labels", hasWritePermission, repository)
<br>
<table class="table table-bordered table-hover table-issues" id="new-label-table" style="display: none;">
<tr><td></td></tr>
</table>
@@ -35,14 +36,14 @@
<script>
$(function(){
$('#new-label-button').click(function(e){
if($('#new-label-area').size() != 0){
if($('#edit-label-area-new').size() != 0){
$('div#edit-label-area-new').remove();
$('#new-label-table').hide();
$('#new-label-area').remove();
} else {
$.get('@url(repository)/issues/labels/new',
function(data){
$('#new-label-table').show().find('tr td').append(data);
}
function(data){
$('#new-label-table').show().find('tr td').append(data);
}
);
}
});

View File

@@ -13,7 +13,7 @@
@import view.helpers._
@html.main((if(target == "issues") "Issues" else "Pull requests") + s" - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu(target, repository){
@tab(target, true, repository)
@navigation(target, true, repository, Some(condition))
@listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
@if(hasWritePermission){
<form id="batcheditForm" method="POST">

View File

@@ -12,6 +12,7 @@
@import context._
@import view.helpers._
@import service.IssuesService.IssueInfo
<br>
@if(condition.nonEmpty){
<div>
<a href="@service.IssuesService.IssueSearchCondition().toURL" class="header-link">
@@ -202,7 +203,7 @@
}
</span>
<div class="small muted" style="margin-left: 40px; margin-top: 5px;">
#@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
#@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(milestoneId = Some(Some(1))).toURL" class="username"><img src="@assets/common/images/milestone.png"> @milestone</a></span>
}

View File

@@ -7,7 +7,7 @@
<h4>New milestone</h4>
<div class="muted">Create a new milestone to help organize your issues and pull requests.</div>
} else {
@issues.html.tab("milestones", false, repository)
@issues.html.navigation("milestones", false, repository)
<br><br>
}
<hr style="margin-top: 12px; margin-bottom: 18px;" class="fill-width"/>

View File

@@ -6,18 +6,19 @@
@import view.helpers._
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){
@html.menu("issues", repository){
@issues.html.tab("milestones", hasWritePermission, repository)
@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">
<img src="@assets/common/images/milestone@(if(state == "open"){"-active"}).png"/>
@milestones.filter(_._1.closedDate.isEmpty).size Open
@milestones.count(_._1.closedDate.isEmpty) Open
</a>&nbsp;&nbsp;
<a class="button-link@if(state == "closed"){ selected}" href="?state=closed">
<img src="@assets/common/images/milestone@(if(state == "closed"){"-active"}).png"/>
@milestones.filter(_._1.closedDate.isDefined).size Closed
@milestones.count(_._1.closedDate.isDefined) Closed
</a>
</span>
</th>
@@ -33,7 +34,7 @@
<a href="@url(repository)/issues?milestone=@milestone.milestoneId&state=open" class="milestone-title">@milestone.title</a>
<div style="margin-top: 6px">
@if(milestone.closedDate.isDefined){
<span class="muted">Closed @datetime(milestone.closedDate.get)</span>
<span class="muted">Closed @helper.html.datetimeago(milestone.closedDate.get)</span>
} else {
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){

View File

@@ -0,0 +1,58 @@
@(active: String,
newButton: Boolean,
repository: service.RepositoryService.RepositoryInfo,
condition: Option[service.IssuesService.IssueSearchCondition] = None)(implicit context: app.Context)
@import context._
@import 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>
<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>
</ul>

View File

@@ -1,30 +0,0 @@
@(active: String, newButton: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import 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>
<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>
@if(loginAccount.isDefined){
<li class="pull-right">
<div class="btn-group">
@if(newButton){
@if(active == "issues"){
<a class="btn btn-success" href="@url(repository)/issues/new">New issue</a>
}
@if(active == "pulls"){
<a class="btn btn-success" href="@url(repository)/compare">New pull request</a>
}
@if(active == "labels"){
<a class="btn btn-success" href="javascript:void(0);" id="new-label-button">New label</a>
}
@if(active == "milestones"){
<a class="btn btn-success" href="@url(repository)/issues/milestones/new">New milestone</a>
}
}
</div>
</li>
}
</ul>

View File

@@ -6,7 +6,7 @@
<head>
<meta charset="utf-8">
<title>@title</title>
<link rel="icon" href="@assets/common/images/favicon.png" type="image/vnd.microsoft.icon" />
<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">
<!-- Le styles -->
<link href="@assets/vendors/bootstrap/css/bootstrap.css" rel="stylesheet">
@@ -18,6 +18,7 @@
<link href="@assets/vendors/datepicker/css/datepicker.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"/>
<link href="@assets/common/css/gitbucket.css" rel="stylesheet">
<script src="@assets/vendors/jquery/jquery-1.9.1.js"></script>
<script src="@assets/vendors/dropzone/dropzone.js"></script>
@@ -29,6 +30,7 @@
<script src="@assets/vendors/google-code-prettify/prettify.js"></script>
<script src="@assets/vendors/zclip/ZeroClipboard.min.js"></script>
<script src="@assets/vendors/elastic/jquery.elastic.source.js"></script>
<script src="@assets/vendors/facebox/facebox.js"></script>
</head>
<body>
<form id="search" action="@path/search" method="POST">
@@ -41,7 +43,7 @@
<span class="icon-bar"></span>
</button>
<a class="brand" href="@path/">
<img src="@assets/common/images/gitbucket.png"/>GitBucket
<img src="@assets/common/images/gitbucket.png" style="width: 24px; height: 24px;"/>GitBucket
@defining(servlet.AutoUpdate.getCurrentVersion){ version =>
<span class="header-version">@version.majorVersion.@version.minorVersion</span>
}
@@ -60,21 +62,11 @@
<li><a href="@path/groups/new">New group</a></li>
</ul>
<a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user"></i></a>
@plugin.PluginSystem.globalMenus.map { menu =>
@if(menu.condition(context)){
<a href="@menu.url" class="menu" data-toggle="tooltip" data-placement="bottom" title="@menu.label">@if(menu.icon.nonEmpty){<img src="@menu.icon" class="plugin-global-menu"/>} else {@menu.label}</a>
}
}
@if(loginAccount.get.isAdmin){
<a href="@path/admin/users" class="menu" data-toggle="tooltip" data-placement="bottom" title="Administration"><i class="icon-wrench"></i></a>
}
<a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a>
} else {
@plugin.PluginSystem.globalMenus.map { menu =>
@if(menu.condition(context)){
<a href="@menu.url" class="menu" data-toggle="tooltip" data-placement="bottom" title="@menu.label">@if(menu.icon.nonEmpty){<img src="@menu.icon" class="plugin-global-menu"/>} else {@menu.label}</a>
}
}
<a href="@path/signin?redirect=@urlEncode(currentPath)" class="btn btn-last" id="signin">Sign in</a>
}
</div><!--/.nav-collapse -->
@@ -88,9 +80,6 @@
$('#search').submit(function(){
return $.trim($(this).find('input[name=query]').val()) != '';
});
@plugin.PluginSystem.javaScripts.filter(_.filter(context.currentPath)).map { js =>
@Html(js.script)
}
});
</script>
</body>

View File

@@ -1,7 +1,10 @@
@(active: String,
repository: service.RepositoryService.RepositoryInfo,
id: Option[String] = None,
expand: Boolean = false)(body: Html)(implicit context: app.Context)
expand: Boolean = false,
isNoGroup: Boolean = true,
info: Option[Any] = None,
error: Option[Any] = None)(body: Html)(implicit context: app.Context)
@import context._
@import view.helpers._
@@ -31,10 +34,20 @@
}
<div class="container">
@helper.html.information(info)
@helper.html.error(error)
@if(repository.commitCount > 0){
<div class="pull-right">
<div class="input-prepend">
<a href="@path/@repository.owner/@repository.name/fork" class="btn btn-small" style="margin-bottom: 10px;">Fork</a>
@if(loginAccount.isEmpty){
<a title="You must be signed in to fork a repository" href="@path/signin" class="btn btn-small" style="margin-bottom: 10px;">Fork</a>
} else {
@if(isNoGroup) {
<a href="@path/@repository.owner/@repository.name/fork" class="btn btn-small" style="margin-bottom: 10px;" data-account="@loginAccount.get.userName">Fork</a>
} else {
<a href="@path/@repository.owner/@repository.name/fork" class="btn btn-small" rel="facebox" style="margin-bottom: 10px;">Fork</a>
}
}
<span class="add-on count"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
</div>
</div>
@@ -61,11 +74,6 @@
@sidemenu("/issues", "issues", "Issues", repository.issueCount)
@sidemenu("/pulls" , "pulls" , "Pull Requests", repository.pullCount)
@sidemenu("/wiki" , "wiki" , "Wiki")
@plugin.PluginSystem.repositoryMenus.map { menu =>
@if(menu.condition(context)){
@sidemenuPlugin(menu.url, menu.label, menu.label, menu.icon)
}
}
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){
@sidemenu("/settings", "settings", "Settings")
}
@@ -171,6 +179,33 @@ $(function(){
$(target).children('img.menu-icon' ).css('display', 'inline');
});
$('a[rel*=facebox]').facebox();
$(document).on("click", ".js-fork-owner-select-target", function() {
if (!$(this).hasClass("disabled")) {
var account = $(this).text().replace("@@", "");
$("#account").val(account);
$("#fork").submit();
}
});
@if(loginAccount.isDefined){
$(document).on("click", "a[data-account]", function(e) {
e.preventDefault();
var form = $('<form/>', {
action: $(this).attr('href'),
method: "post"
});
var account = $('<input/>', {
type: "hidden",
name: "account",
value: $(this).data('account')
});
form.append(account);
form.submit();
});
}
@if(settings.ssh && loginAccount.isDefined){
$('#repository-url-http').click(function(){
$('#repository-url-proto').text('HTTP');

View File

@@ -1,4 +1,5 @@
@(commits: Seq[Seq[util.JGitUtil.CommitInfo]],
comments: Option[List[model.Comment]] = None,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@@ -15,6 +16,14 @@
@user(commit.authorName, commit.authorEmailAddress, "username")
</td>
<td>@commit.shortMessage</td>
<td style="width: 10%; text-align: right">
<span class="badge" style="display: inline">@if(comments.isDefined){
@comments.get.flatMap @{
case comment: model.CommitComment => Some(comment)
case other => None
}.count(t => t.commitId == commit.id && !t.pullRequest)
}</span>
</td>
<td style="width: 10%; text-align: right;">
<a href="@url(repository)/commit/@commit.id" class="monospace">@commit.id.substring(0, 7)</a>
</td>

View File

@@ -1,6 +1,7 @@
@(commits: Seq[Seq[util.JGitUtil.CommitInfo]],
diffs: Seq[util.JGitUtil.DiffInfo],
members: List[(String, String)],
comments: List[model.Comment],
originId: String,
forkedId: String,
sourceId: String,
@@ -45,7 +46,7 @@
</div>
@if(commits.nonEmpty && hasWritePermission){
<div style="margin-bottom: 10px;" id="create-pull-request">
<a href="#" class="btn" id="show-form">Click to create a pull request for this comparison</a>
<a href="#" class="btn btn-success" id="show-form">Create pull request</a>
</div>
<div id="pull-request-form" class="box" style="display: none;">
<div class="box-content">
@@ -58,7 +59,7 @@
<div style="width: 600px; border-right: 1px solid #d4d4d4;">
<span class="error" id="error-title"></span>
<input type="text" name="title" style="width: 580px" placeholder="Title"/>
@helper.html.preview(repository, "", false, true, "width: 580px; height: 200px;")
@helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 580px; height: 200px;")
<input type="hidden" name="targetUserName" value="@originRepository.owner"/>
<input type="hidden" name="targetBranch" value="@originId"/>
<input type="hidden" name="requestUserName" value="@forkedRepository.owner"/>
@@ -81,8 +82,10 @@
</tr>
</table>
} else {
@pulls.html.commits(commits, repository)
@helper.html.diff(diffs, repository, Some(commitId), Some(sourceId), true)
@pulls.html.commits(commits, Some(comments), repository)
@helper.html.diff(diffs, repository, Some(commitId), Some(sourceId), true, None, hasWritePermission, false)
<p>Showing you all comments on commits in this comparison.</p>
@issues.html.commentlist(None, comments, hasWritePermission, repository, None)
}
}
}

View File

@@ -1,6 +1,6 @@
@(issue: model.Issue,
pullreq: model.PullRequest,
comments: List[model.IssueComment],
comments: List[model.Comment],
issueLabels: List[model.Label],
collaborators: List[String],
milestones: List[(model.Milestone, Int, Int)],
@@ -8,11 +8,17 @@
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import model.IssueComment
@import view.helpers._
<div class="row-fluid">
<div class="span10">
@issues.html.commentlist(issue, comments, hasWritePermission, repository, Some(pullreq))
@defining(comments.exists(_.action == "merge")){ merged =>
<div id="comment-list">
@issues.html.commentlist(Some(issue), comments, hasWritePermission, repository, Some(pullreq))
</div>
@defining(comments.flatMap {
case comment: IssueComment => Some(comment)
case other => None
}.exists(_.action == "merge")){ merged =>
@if(hasWritePermission && !issue.closed){
<div class="box issue-comment-box" style="background-color: #d8f5cd;">
<div class="box-content"class="issue-content" style="border: 1px solid #95c97e; padding: 10px;">
@@ -48,24 +54,11 @@
<span class="small muted">You're all set-the <span class="label label-info monospace">@pullreq.requestBranch</span> branch can be safely deleted.</span>
</div>
</div>
}
@issues.html.commentform(issue, !merged, hasWritePermission, repository)
</div>
<div class="span2">
@if(issue.closed) {
@if(merged){
<span class="label label-info issue-status">Merged</span>
} else {
<span class="label label-important issue-status">Closed</span>
}
} else {
<span class="label label-success issue-status">Open</span>
@issues.html.commentform(issue, !merged, hasWritePermission, repository)
}
<div class="small" style="text-align: center;">
<span class="strong">@comments.size</span> @plural(comments.size, "comment")
</div>
}
<hr/>
</div>
<div class="span2">
@issues.html.issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
</div>
</div>

View File

@@ -6,4 +6,4 @@
<h4 style="color: #468847;">Able to merge</h4>
<p>These branches can be automatically merged.</p>
}
<input type="submit" class="btn btn-success btn-block" value="Send pull request"/>
<input type="submit" class="btn btn-success btn-block" value="Create pull request"/>

View File

@@ -36,7 +36,7 @@
</p>
}
@helper.html.copy("repository-url-copy", requestRepositoryUrl){
<input type="text" value="@requestRepositoryUrl" id="repository-url" readonly>
<input type="text" style="width: 500px;" value="@requestRepositoryUrl" id="repository-url" readonly>
}
<div>
<p>
@@ -62,7 +62,7 @@
<p>
<span class="strong">Step 3:</span> Merge the changes and update the server
</p>
@defining(s"git checkout master\ngit merge ${pullreq.requestUserName}-${pullreq.requestBranch}\ngit push origin ${pullreq.branch}"){ command =>
@defining(s"git checkout ${pullreq.branch}\ngit merge ${pullreq.requestUserName}-${pullreq.requestBranch}\ngit push origin ${pullreq.branch}"){ command =>
@helper.html.copy("merge-command-copy-3", command){
<pre style="width: 500px; float: left;">@command</pre>
}

View File

@@ -1,6 +1,6 @@
@(issue: model.Issue,
pullreq: model.PullRequest,
comments: List[model.IssueComment],
comments: List[model.Comment],
issueLabels: List[model.Label],
collaborators: List[String],
milestones: List[(model.Milestone, Int, Int)],
@@ -14,26 +14,58 @@
@html.main(s"${issue.title} - Pull Request #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("pulls", repository){
@defining(dayByDayCommits.flatten){ commits =>
<div class="pullreq-info">
@if(issue.closed) {
@comments.find(_.action == "merge").map{ comment =>
<span class="label label-info">Merged</span>
<div>
<div class="show-title pull-right">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a class="btn btn-small" href="#" id="edit">Edit</a>
}
<a class="btn btn-small 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>
</div>
<h1>
<span class="show-title">
<span id="show-title">@issue.title</span>
<span class="muted">#@issue.issueId</span>
</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"/>
</span>
</h1>
</div>
@if(issue.closed) {
@comments.flatMap @{
case comment: model.IssueComment => Some(comment)
case _ => None
}.find(_.action == "merge").map{ comment =>
<span class="label label-info issue-status">Merged</span>
<span class="muted">
@user(comment.commentedUserName, styleClass="username strong") merged @commits.size @plural(commits.size, "commit")
into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
at @datetime(comment.registeredDate)
}.getOrElse {
<span class="label label-important">Closed</span>
@helper.html.datetimeago(comment.registeredDate)
</span>
}.getOrElse {
<span class="label label-important issue-status">Closed</span>
<span class="muted">
@user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit")
into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
}
} else {
<span class="label label-success">Open</span>
</span>
}
} else {
<span class="label label-success issue-status">Open</span>
<span class="muted">
@user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit")
into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
}
</div>
</span>
}
<br/><br/>
<ul class="nav nav-tabs fill-width pull-left" id="pullreq-tab">
<li class="active"><a href="#conversation">Conversation <span class="badge">@comments.size</span></a></li>
<li class="active"><a href="#conversation">Conversation <span class="badge">@comments.flatMap @{
case comment: model.IssueComment => Some(comment)
case _: model.CommitComment => None
}.size</span></a></li>
<li><a href="#commits">Commits <span class="badge">@commits.size</span></a></li>
<li><a href="#files">Files Changed <span class="badge">@diffs.size</span></a></li>
</ul>
@@ -42,18 +74,51 @@
@pulls.html.conversation(issue, pullreq, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
</div>
<div class="tab-pane" id="commits">
@pulls.html.commits(dayByDayCommits, repository)
@pulls.html.commits(dayByDayCommits, Some(comments), repository)
</div>
<div class="tab-pane" id="files">
@helper.html.diff(diffs, repository, Some(commits.head.id), Some(commits.last.id), true)
@helper.html.diff(diffs, repository, Some(commits.head.id), Some(commits.last.id), true, Some(pullreq.issueId), hasWritePermission, true)
</div>
</div>
}
}
}
<script>
$('#pullreq-tab a').click(function (e) {
e.preventDefault();
$(this).tab('show');
$(function(){
$('#pullreq-tab a').click(function (e) {
e.preventDefault();
$(this).tab('show');
});
$('#edit').click(function(){
$('.edit-title').show();
$('.show-title').hide();
return false;
});
$('#update').click(function(){
$(this).attr('disabled', 'disabled');
$.ajax({
url: '@url(repository)/issues/edit_title/@issue.issueId',
type: 'POST',
data: {
title : $('#edit-title').val()
}
}).done(function(data){
$('#show-title').empty().text(data.title);
$('#cancel').click();
$(this).removeAttr('disabled');
}).fail(function(req){
$(this).removeAttr('disabled');
$('#error-edit-title').text($.parseJSON(req.responseText).title);
});
return false;
});
$('#cancel').click(function(){
$('.edit-title').hide();
$('.show-title').show();
return false;
});
});
</script>

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