Compare commits

..

232 Commits
1.4 ... 1.6

Author SHA1 Message Date
Naoki Takezoe
9d71d39917 Update README.md 2013-09-29 16:31:12 +09:00
takezoe
5430564065 (refs #105)Specify suitable Content-Type header for downloaded file. 2013-09-29 16:27:40 +09:00
shimamoto
54bc8c16d8 (refs #100) Fix bug that can't get ServletContext in the future block. 2013-09-28 21:03:55 +09:00
takezoe
0affdb6ad0 Bug fix caused by path splitting. 2013-09-27 14:33:27 +09:00
takezoe
532978522a Fix logback configuration. 2013-09-27 14:07:20 +09:00
Naoki Takezoe
05a9a0b45c Update README.md 2013-09-27 11:32:15 +09:00
Naoki Takezoe
24f8ad11ad Update README.md 2013-09-27 11:28:05 +09:00
Naoki Takezoe
ce943a0e6c Update README.md 2013-09-27 11:26:06 +09:00
takezoe
204c0cd0f8 (refs #96)Add --port and --prefix option. 2013-09-27 03:05:05 +09:00
takezoe
c213008f1c (refs #96)Small fix for build.xml. 2013-09-27 02:45:00 +09:00
takezoe
e6ad069509 (refs #96)Improve Jetty embedding process. 2013-09-27 02:43:22 +09:00
takezoe
38c7e3cdf8 (refs #96)Add build.xml which makes an executable war file. 2013-09-26 20:17:33 +09:00
takezoe
2be79f6590 Replace trace log with debug log. 2013-09-26 12:02:34 +09:00
takezoe
2f7125b6c0 Add trace log to WebHookService to check future execution. 2013-09-25 13:12:35 +09:00
takezoe
bb03a6fc9b (refs #94)The merge-guide is separated as HTML fragment and retrieve them by Ajax. 2013-09-23 13:25:06 +09:00
takezoe
7b774aee1a Use helper.html.dropdown() instead of the direct Bootstrap use. 2013-09-23 03:13:21 +09:00
takezoe
d53619c247 Small fix. 2013-09-23 02:14:31 +09:00
takezoe
d34118bdfd Define request attribute keys. 2013-09-23 02:03:10 +09:00
takezoe
c57bc487a3 Define session keys. 2013-09-23 00:51:57 +09:00
takezoe
296fc9a3df Improve session handling. 2013-09-23 00:18:38 +09:00
takezoe
fd8b5780f3 Use ControlUtil. 2013-09-22 19:28:14 +09:00
takezoe
602b6c635a Add RichRequest which extends HttpServletRequest. 2013-09-22 14:25:50 +09:00
takezoe
a79180699e Generalize the account completion field. 2013-09-22 13:35:05 +09:00
takezoe
e9901a8abf Generalize the commit list in the pull request. 2013-09-22 04:19:41 +09:00
takezoe
4e63d64c13 Generalize the file index of diff. 2013-09-22 04:05:51 +09:00
takezoe
4261b7adbe Use .strong instead of <strong>. 2013-09-22 03:27:18 +09:00
takezoe
f30c9f6171 Use ControlUtil. 2013-09-22 01:24:04 +09:00
takezoe
c00b704843 Use ControlUtil#using() to handle RevWalk. 2013-09-21 22:21:59 +09:00
takezoe
e89b2020a3 Use ControlUtil. 2013-09-21 22:13:15 +09:00
Naoki Takezoe
18ca3cbd80 Update README.md 2013-09-20 13:48:37 +09:00
takezoe
062d6cd066 Add ControlUtil. 2013-09-19 18:53:50 +09:00
takezoe
b4dd067d61 Introduce ControlUtil which provides control facilities such as using() or defining(). 2013-09-19 18:53:14 +09:00
takezoe
fd22e2911a Remove debug code. 2013-09-19 13:26:33 +09:00
takezoe
73d9e69e43 (refs #74)Small fix for test hook. 2013-09-19 02:40:07 +09:00
takezoe
7e4c29f4cf (refs #74)Remove an auxiliary constructor from case class because json4s can't serialize correctly if case class have that. 2013-09-19 00:47:46 +09:00
takezoe
32672262ef Merge branch 'master' of https://github.com/takezoe/gitbucket 2013-09-18 20:13:17 +09:00
takezoe
3c865ea20b (refs #74)Remove an unnecessary action and add TODO. 2013-09-18 20:12:47 +09:00
takezoe
d8698d02b7 (refs #74)Add "Test Hook" button. 2013-09-18 20:10:53 +09:00
shimamoto
d5b47e5adb Merge branch 'master' of https://github.com/takezoe/gitbucket.git 2013-09-18 19:22:25 +09:00
shimamoto
accb1cf2ab Refactoring. 2013-09-18 19:21:56 +09:00
takezoe
aa8da1b046 Fix TODO. 2013-09-18 15:28:59 +09:00
takezoe
c52ed32949 Fix whitespaces. 2013-09-18 15:27:25 +09:00
takezoe
ec6f4ff734 Fix typo. 2013-09-18 14:30:41 +09:00
takezoe
06b0dbf2e5 Remove TODO. 2013-09-18 14:24:15 +09:00
takezoe
98d24248c2 Add ExecutionContext for Future. 2013-09-14 18:34:05 +09:00
takezoe
cec1dc98a9 (refs #74)Web hook request is sent asynchronously. 2013-09-14 17:43:06 +09:00
takezoe
36115734bb (refs #74)Web hook is completed. 2013-09-14 17:14:37 +09:00
takezoe
c1eccd391d (refs #74)JSON conversion test. 2013-09-13 21:58:50 +09:00
takezoe
7fe86fcdb2 Merge branch 'webhook' of https://github.com/takezoe/gitbucket into webhook 2013-09-13 19:07:09 +09:00
takezoe
7f81ec52c1 (refs #74)JSON conversion test. 2013-09-13 19:06:45 +09:00
takezoe
7c269de39b (refs #74)Implementing conversion of web hook payload. 2013-09-13 03:24:34 +09:00
takezoe
aa9e34e992 (refs #74)Added case classes for payload of web hook. 2013-09-12 12:57:07 +09:00
takezoe
4d0ab514fb (refs #74)Remove web hook URL is available. 2013-09-12 08:41:26 +09:00
takezoe
9d526b32e0 Delete from WEB_HOOK before deleting repository. 2013-09-12 01:38:52 +09:00
takezoe
90a83c5c64 Merge branch 'master' into webhook 2013-09-12 01:34:31 +09:00
takezoe
e6e5cc67d5 Delete from PULL_REQUEST before deleting repository. 2013-09-11 18:05:34 +09:00
takezoe
4a6eb95474 Fix redirect path to the context root. 2013-09-11 03:53:50 +09:00
takezoe
7bce8cf3b6 Fix redirect path after sign in. 2013-09-11 03:40:55 +09:00
takezoe
4d1605ded2 (refs #74)Add web hook URL addition. 2013-09-06 02:32:51 +09:00
Naoki Takezoe
2bec2cfa93 Merge pull request #95 from kaakaa/fix-activity-bug
Fix a problem in making link to commit in activities
2013-09-05 09:24:32 -07:00
kaakaa
ff07872a3d Fix a problem in making link to commit in activities 2013-09-05 22:34:51 +09:00
takezoe
35733cd82e Merge branch 'master' into webhook 2013-09-05 14:53:58 +09:00
takezoe
c88b051121 Rolled back d84d40afea 2013-09-05 14:46:58 +09:00
takezoe
38df990033 Merge branch 'master' into webhook 2013-09-05 02:35:58 +09:00
Naoki Takezoe
c7776b5b37 Update README.md 2013-09-04 18:46:09 +09:00
Naoki Takezoe
f89afc175f Update README.md 2013-09-04 18:45:11 +09:00
shimamoto
1f252efdfb Merge branch 'master' of https://github.com/takezoe/gitbucket.git 2013-09-04 18:46:04 +09:00
shimamoto
420ca85393 (refs #10) Except group account in notification. 2013-09-04 18:45:37 +09:00
Naoki Takezoe
d60695992b Update README.md 2013-09-04 11:27:14 +09:00
shimamoto
3c0681d55d (refs #10) Add notification of when merged. 2013-09-04 10:40:44 +09:00
takezoe
3fc0fa5a02 Don't set response content type via Accept header. 2013-09-04 03:39:28 +09:00
takezoe
d84d40afea Set init parameters using ServletContext#setInitParameter(). 2013-09-04 02:19:04 +09:00
Naoki Takezoe
ddbbd38517 Merge pull request #93 from tanacasino/feature/configurable-data-dir-2
Make configurable data(git,db) dir using env vars
2013-09-03 10:04:31 -07:00
Tomofumi Tanaka
d588531ab8 Make configurable data(git,db) dir using env vars 2013-09-04 00:11:13 +09:00
takezoe
bdc06feb88 Fix a problem in pull request to branches other than the master branch. 2013-09-03 20:58:38 +09:00
takezoe
940e2f4759 (refs #74)Add WEB_HOOK table. 2013-09-02 18:17:28 +09:00
shimamoto
3fc792fcf8 (refs #10) Add notification. Notification of when merging is not
implemented yet.
2013-09-02 07:40:41 +09:00
shimamoto
f5520e7991 (refs #10) Completed notification implementation. 2013-09-01 21:17:55 +09:00
shimamoto
897c5ecac7 Fix default smtp port in constant. 2013-09-01 21:15:17 +09:00
takezoe
4479ef31e2 (refs #90)Display validation error message for Default Branch. 2013-08-31 02:28:58 +09:00
takezoe
ec827ab371 Remove unused import statement. 2013-08-31 02:11:13 +09:00
takezoe
6fe65c76b1 (refs #74)Add the web hook configuration page. 2013-08-28 13:21:51 +09:00
takezoe
e79d463cf7 (refs #87)rolled back because it breaks content type for resources such as css or images. 2013-08-25 22:12:00 +09:00
Naoki Takezoe
1508e5db49 Update README.md 2013-08-25 19:32:50 +09:00
Naoki Takezoe
8de391825a Merge pull request #87 from odz/support-ie
Specify ContentType
2013-08-24 21:36:51 -07:00
odz
da1172a882 specify contentType 2013-08-24 22:30:58 +09:00
Naoki Takezoe
288a434598 Update README.md 2013-08-24 13:33:52 +09:00
Naoki Takezoe
1d6ae1e589 Update README.md 2013-08-24 13:33:24 +09:00
Naoki Takezoe
dd29456384 Merge pull request #86 from odz/chardet
Add encoding detection
2013-08-23 21:24:41 -07:00
takezoe
95a658defa Separate ZeroClipboard button as the helper. 2013-08-24 13:22:17 +09:00
takezoe
cd298eb5c1 bindDN and bindPassword became optional for OpenLDAP. 2013-08-24 03:06:19 +09:00
takezoe
f7de3bab74 Fix LDAPUtil#findUser() for OpenLDAP. 2013-08-24 01:45:30 +09:00
odz
13578dcee8 Add encoding detection 2013-08-24 00:54:40 +09:00
shimamoto
6d76e93ede (refs #10) Creates a E-mail sending. still working on... 2013-08-23 22:30:40 +09:00
takezoe
6b57cca64d Merge branch 'ldap-auth' 2013-08-22 02:31:24 +09:00
takezoe
e0bd5a24f4 Fix indent. 2013-08-22 02:29:05 +09:00
takezoe
2b2bf88a37 Scalized :-) 2013-08-22 02:27:45 +09:00
Naoki Takezoe
a0fa53e8cb Merge pull request #82 from tanacasino/ldap-auth-use-bind-account
LDAP authentication by using bind account
2013-08-21 10:04:26 -07:00
Naoki Takezoe
c841d4a77a Merge pull request #83 from tanacasino/sbt-sh
Add sbt.sh for UNIX users
2013-08-21 05:10:01 -07:00
Tomofumi Tanaka
bf4b2dc72c Add sbt.sh for UNIX users 2013-08-21 20:15:55 +09:00
Tomofumi Tanaka
078ed868fb Fix indent 2013-08-21 20:08:27 +09:00
Tomofumi Tanaka
bfc1d1d6b0 LDAP authentication by using bind account 2013-08-21 19:49:43 +09:00
takezoe
42ecae944e Remove unused import statements. 2013-08-17 11:11:31 +09:00
takezoe
b9aa6a234b (refs #78)Authentication moved to AccountService. 2013-08-17 11:05:11 +09:00
takezoe
5f2d62030f (refs #77)Display issue count and pull request count on the global nav. 2013-08-17 02:55:33 +09:00
takezoe
fd0169d012 Fix presentation. 2013-08-17 02:53:42 +09:00
takezoe
7e26b4695d (refs #78)LDAP port is optional. 2013-08-17 01:48:01 +09:00
takezoe
cdfdff5c32 (refs #78)LDAP authenticated user can't set password. 2013-08-17 01:16:22 +09:00
takezoe
df5600f03f (refs #78)Fix for LDAP authentication. 2013-08-17 01:10:06 +09:00
takezoe
231fd268df (refs #78)LDAP authentication is completed? (not tested yet) 2013-08-16 11:46:16 +09:00
takezoe
582df3239f (refs #78)Implementing LDAP authentication. 2013-08-16 03:45:50 +09:00
takezoe
3ea102e238 Upgrade Scalatra to 2.2.1. 2013-08-15 11:22:10 +09:00
takezoe
52ab3c625e (refs #76)Show the content of the previous commit for removed files. 2013-08-15 02:22:11 +09:00
takezoe
dee13542cd Remove unused import statements. 2013-08-15 01:14:44 +09:00
Naoki Takezoe
e90ba9e65b Merge pull request #75 from tanacasino/fix/blob-view
Ensure display file content of specified commit
2013-08-14 08:49:18 -07:00
Tomofumi Tanaka
ca86076a02 Ensure display file content of specified commit 2013-08-15 00:21:55 +09:00
takezoe
6c75a29cb0 Fix small gap of a icon part. 2013-08-11 00:52:41 +09:00
takezoe
e10777576f Comparing is accessible by users who can refer to the repository. 2013-08-11 00:47:23 +09:00
takezoe
08eaf2104b (refs #23)Add "Branch" tab to the repository viewer. 2013-08-11 00:34:33 +09:00
takezoe
14de86afa0 Fix redirect behaviour after sign in. 2013-08-10 23:13:43 +09:00
takezoe
69c5f9e19a Always display repository selector in the new pull request page. 2013-08-10 12:34:07 +09:00
takezoe
03e903eef9 Improved the list of forked repositories presentation. 2013-08-10 04:21:31 +09:00
takezoe
f3a1815bc5 Add "Network" to the global navigation. 2013-08-10 03:51:31 +09:00
takezoe
ef03f77dc9 Remove unnecessary foreign key constraint. 2013-08-10 03:50:28 +09:00
takezoe
1a509a9a13 Use released scalatra-forms 0.0.2. 2013-08-10 02:27:07 +09:00
takezoe
1e566f4a20 (refs #69)Forked repositories tree is changed to flat list.
Because it can't render forked tree correctly if parent repository has been removed.
2013-08-09 21:43:30 +09:00
takezoe
709c8f32b5 (refs #69)Remove PULL_REQUEST table's foreign key for REQUEST_USER_NAME and REQUEST_REPOSITORY_NAME. 2013-08-09 18:47:44 +09:00
takezoe
f2787a547f Add "View the diff" link to the edit wiki page activity. 2013-08-09 18:38:34 +09:00
takezoe
629b714dab Upgrade scalatra-forms. 2013-08-09 18:06:33 +09:00
takezoe
1b0269c567 Fix default label creation for group repository. 2013-08-09 12:18:51 +09:00
shimamoto
6158dc9607 Fix header activation of milestones. 2013-08-08 21:15:25 +09:00
shimamoto
5462f0a7a1 Merge branch 'master' of https://github.com/takezoe/gitbucket.git 2013-08-08 20:59:27 +09:00
shimamoto
6d453ea80b (refs #10) Add notification email form. 2013-08-08 20:58:57 +09:00
takezoe
5952648fae Clean up CSS styles in activity timeline. 2013-08-08 03:01:09 +09:00
takezoe
6b49bd557f (refs #2)Add header which shows pull request information to the pull request detail page. 2013-08-08 02:56:59 +09:00
takezoe
c071284a56 (refs #2)Recover "New Request" button which has been removed temporary while implementing dashboard. 2013-08-08 02:06:55 +09:00
takezoe
5930cf48d5 (refs #2)Fix redirect path after sending pull request. 2013-08-08 02:04:55 +09:00
takezoe
9dd070887a (refs #2)Merge comment is displayed on the comment list (but it's not included in comment count). 2013-08-08 01:42:42 +09:00
takezoe
cf687a0f2c Add activity icons to SVG file. 2013-08-07 21:19:06 +09:00
takezoe
f5c0cfdcdd Rename functions. 2013-08-07 21:17:57 +09:00
takezoe
03e2974709 Fix activity timeline. 2013-08-07 21:12:28 +09:00
takezoe
d2373a00ea Add icon for create tag activity. 2013-08-07 21:02:35 +09:00
takezoe
e769460397 Add icons for activity. 2013-08-07 18:14:54 +09:00
takezoe
a0a284ad26 (refs #2)Fix comment to pull request activity. 2013-08-07 04:13:14 +09:00
takezoe
1ebf4276e7 (refs #2)'Pull Requests' tab in dashboard has been completed. 2013-08-07 03:31:26 +09:00
takezoe
908931b9ed (refs #2)Implementing 'Pull Requests' tab in the dashboard. 2013-08-06 22:04:09 +09:00
takezoe
50655d1ac2 (refs #2)Fix merge message for the pull request from same repository. 2013-08-06 13:29:15 +09:00
Naoki Takezoe
92e19ee19f Merge pull request #72 from takezoe/logo-icon
(refs #49)Add favicon and header logo
2013-08-05 20:33:25 -07:00
takezoe
52f3a90d18 (refs #2)Fix merged message in the comment list. 2013-08-06 12:34:28 +09:00
takezoe
11371c9e4f Update image for no image users. 2013-08-06 08:36:25 +09:00
takezoe
1b71b81953 (refs #71)Fix authentication for forking repository. 2013-08-06 08:17:19 +09:00
takezoe
c9d9d22215 (refs #49)Add favicon and header logo. Thanks to @hansgru! 2013-08-06 08:07:51 +09:00
Naoki Takezoe
5300641822 Merge pull request #70 from takezoe/toggle_gravatar
Toggle gravatar support
2013-08-05 10:11:21 -07:00
takezoe
b31b7e1e86 Merge branch 'master' into toggle_gravatar
Conflicts:
	src/main/scala/view/AvatarImageProvider.scala
2013-08-06 01:58:47 +09:00
takezoe
cfb2f5beb9 Add SVG file. 2013-08-05 22:22:30 +09:00
Naoki Takezoe
ee9f24b2a6 Merge pull request #67 from takezoe/fork-and-pullreq
Fork and Pull Request
2013-08-05 06:09:12 -07:00
takezoe
8c86e23a4c (refs #2)HTML parts sharing in issues and pull requests. 2013-08-05 21:57:45 +09:00
takezoe
fe98d35d4e (refs #2)Fix redirect path for pull request. 2013-08-05 21:06:42 +09:00
takezoe
8e10693402 (refs #2)Don't display reopen button for the pull request. 2013-08-05 18:53:04 +09:00
takezoe
f31848721c Remove unnecessary comment and format code. 2013-08-05 18:49:08 +09:00
takezoe
6101e141d8 (refs #2)Add opened user filter and count to the pull request list. 2013-08-05 18:47:40 +09:00
takezoe
71d84e7475 (refs #2)Limit of pull request list is 25. 2013-08-05 16:34:11 +09:00
takezoe
735ad4c972 Fix comment. 2013-08-05 15:19:16 +09:00
takezoe
50cb59f569 (refs #2)Add action type "merge" for ISSUE_COMMENT. 2013-08-05 15:16:26 +09:00
takezoe
b58c19b88b (refs #2)Add issue and pull request icon. 2013-08-05 14:40:06 +09:00
Naoki Takezoe
6fe9ebbd2d Update README.md 2013-08-05 03:41:25 +09:00
takezoe
4ea23a96ae (refs #2)Implementing pull request list. 2013-08-05 03:31:27 +09:00
takezoe
ebf5d00fd2 (refs #2)Fix link for pull requests. 2013-08-05 02:11:06 +09:00
takezoe
b015645ed0 (refs #2)Add flag for identifying whether it's a pull request. 2013-08-05 02:06:15 +09:00
takezoe
ce3b10ef03 (refs #2)Restore checkConflict method. 2013-08-05 01:35:08 +09:00
takezoe
d7af5551eb (refs #2)Fix temporary branch name. 2013-08-05 00:53:30 +09:00
takezoe
1d03a82be4 (refs #2)Pull request works! 2013-08-05 00:49:09 +09:00
takezoe
aa5fdfa395 Merge branch 'master' into fork-and-pullreq 2013-08-04 13:13:44 +09:00
takezoe
7e05bcc81d Use released scalatra-forms 0.0.1. The jar file in /lib has been removed. 2013-08-04 13:13:18 +09:00
takezoe
e52aa7ad3c Merge branch 'master' into fork-and-pullreq
Conflicts:
	src/main/scala/app/RepositoryViewerController.scala
	src/main/scala/service/RepositoryService.scala
2013-08-04 05:10:01 +09:00
Naoki Takezoe
42faf9bda2 Merge pull request #59 from tanacasino/fix/issue-58
(fix #58) Fix bug that failed to view tag tree
2013-08-03 12:46:58 -07:00
takezoe
984164ba60 Applied schema changes until 1.4 to the ER diagram. 2013-08-04 03:10:47 +09:00
Tomofumi Tanaka
d26faac0e6 (fix #58) Fix bug that failed to view tag tree 2013-08-04 01:28:47 +09:00
shimamoto
b54a9ace9f Merge pull request #56 from kxbmap/fix-uoe
Fix UnsupportedOperationException
2013-07-31 21:21:09 -07:00
shimamoto
ad0131de66 Remove proxy settings. 2013-08-01 12:54:18 +09:00
Naoki Takezoe
b9ac48ebef Merge pull request #57 from kxbmap/upgrade-sbt-idea
Upgrade to sbt-idea 1.5.1
2013-07-31 16:32:51 -07:00
kxbmap
71751ae4bc Fix an error that occurs when a new user accesses to dashboard/issues/repos 2013-08-01 03:36:15 +09:00
kxbmap
1c6f4a1d1e Upgrade to sbt-idea 1.5.1 2013-08-01 02:15:56 +09:00
takezoe
7a8958741d (refs #2)Add NO_FF option to merging pull request. 2013-07-29 02:10:21 +09:00
takezoe
f317d74bb4 (refs #2)Pull request to the branch in the same repository is available. 2013-07-27 13:02:22 +09:00
takezoe
5f0eb91a81 (refs #2)Compare to its own branch if repository is not specified. 2013-07-27 04:24:58 +09:00
takezoe
66f3a1fe7d (refs #2)Comparing between all forked repositories. 2013-07-27 04:11:33 +09:00
takezoe
59d85531ce Bugfix 2013-07-26 18:29:00 +09:00
takezoe
4bd4c3e833 Merge branch 'master' into fork-and-pullreq
Conflicts:
	src/main/scala/app/CreateRepositoryController.scala
	src/main/scala/util/JGitUtil.scala
2013-07-26 18:22:14 +09:00
takezoe
47f082e2fc Remove unused code. 2013-07-26 18:15:19 +09:00
takezoe
1af52d16c0 Add lock for repository operation. 2013-07-26 18:14:31 +09:00
takezoe
2f52ed3ee0 (refs #2)Fork repository can not be changed repository type. 2013-07-26 10:01:28 +09:00
takezoe
a09407da8e Remove TODO. 2013-07-26 09:47:22 +09:00
takezoe
e15bd77789 (refs #2)Add forked count and repository tree view. 2013-07-25 20:47:35 +09:00
takezoe
b61836adf7 Toggle Gravatar support at the system settings. 2013-07-25 03:00:46 +09:00
takezoe
88caff38f0 (refs #2)Fix pull request. Basic pattern had been tested but it's still unstable. 2013-07-24 22:05:36 +09:00
takezoe
205119cc01 (refs #2)Fix compile errors. 2013-07-24 13:33:07 +09:00
takezoe
f10f98abf2 Merge branch 'master' into fork-and-pullreq
Conflicts:
	src/main/resources/update/1_3.sql
	src/main/resources/update/1_4.sql
	src/main/scala/app/CreateRepositoryController.scala
	src/main/scala/service/WikiService.scala
	src/main/twirl/account/repositories.scala.html
2013-07-24 13:29:23 +09:00
takezoe
2c52a4c40c (refs #2)Fix pull request and marge behavior. 2013-07-16 21:02:22 +09:00
takezoe
f53f71ecf1 (refs #2)Add conflict checking. 2013-07-16 03:43:26 +09:00
takezoe
e59ae9c6e9 (refs #2)Remove unused code. 2013-07-16 00:29:30 +09:00
takezoe
aae40a7087 Bugfix 2013-07-16 00:11:45 +09:00
takezoe
9f9148fc1f (refs #2)Display commit and diff count on the tab. 2013-07-15 23:07:09 +09:00
takezoe
20e5832ce3 (refs #2)Pull request details page became a single page. 2013-07-15 23:02:54 +09:00
takezoe
fc29b34573 (refs #2)Fix comparing diffs before sending pull request. 2013-07-15 21:40:54 +09:00
takezoe
5ea9150af8 (refs #2)Fix requested repository url in the merge guidance. 2013-07-15 14:40:50 +09:00
takezoe
159a5835e0 (refs #2)Close issue when pull request is merged. 2013-07-15 14:17:26 +09:00
takezoe
78d48c8be3 Remove var! 2013-07-15 13:02:26 +09:00
takezoe
9bb6b216e9 (refs #2)Add columns MERGE_START_ID and MERGE_END_ID to PULL_REQUEST. 2013-07-15 04:49:14 +09:00
takezoe
dc59d1f3ca (refs #2)Display forked repository at the repository list of the account page. 2013-07-15 03:49:43 +09:00
takezoe
1ab3f53a31 Merge branch 'fork-and-pullreq' of https://github.com/takezoe/gitbucket into fork-and-pullreq 2013-07-15 03:48:06 +09:00
takezoe
fd7d387fb0 (refs #2)Experimental implementation of merge pull request. 2013-07-15 03:47:43 +09:00
takezoe
17a64506f8 (refs #3)Experimental implementation of merge pull request. 2013-07-15 03:23:28 +09:00
takezoe
b68977597b (refs #2)Add tabs to the pull request page. 2013-07-14 14:06:48 +09:00
takezoe
2fb9f83227 (refs #2)Add merge pull request form. 2013-07-14 12:49:49 +09:00
takezoe
6fd312f784 Formatted. 2013-07-14 03:29:17 +09:00
takezoe
12d59231c5 (refs #2)Record 'open pull request' activity. 2013-07-14 03:28:37 +09:00
takezoe
3a7e2c0249 (refs #2)Record 'open pull request' activity. 2013-07-14 03:27:59 +09:00
takezoe
62f2defd91 Fix typo. 2013-07-14 03:23:35 +09:00
takezoe
9048e07b6b (refs #2)Add the details page for the pull request. 2013-07-14 03:01:46 +09:00
takezoe
0903721a62 (refs #2)Create pull request is available. 2013-07-14 02:43:45 +09:00
takezoe
bf3380755b (refs #2)Fix PULL_REQUEST schema. 2013-07-14 01:25:27 +09:00
takezoe
5d327ccd53 (refs #2)Implementing comparing settings. 2013-07-13 23:07:36 +09:00
takezoe
eb82af9006 (refs #2)Implementing the comparing view. 2013-07-13 20:09:19 +09:00
takezoe
2cc2902930 (refs #2)Comparing between the forked repository and the source repository. 2013-07-13 03:52:27 +09:00
takezoe
f4cb0625bc (refs #2)Add PullRequest model. 2013-07-12 16:37:58 +09:00
takezoe
edd40ebe9d (refs #2)Add 'Pull Requests' tab to the header. 2013-07-12 16:30:30 +09:00
takezoe
0760b6a89c Merge branch 'master' into fork-and-pullreq
Conflicts:
	src/main/scala/app/CreateRepositoryController.scala
	src/main/scala/service/WikiService.scala
	src/main/scala/util/JGitUtil.scala
2013-07-12 15:50:53 +09:00
takezoe
07cdc6002d Remove debug code. 2013-07-11 19:03:55 +09:00
takezoe
ee6d17d165 Add TODO. 2013-07-11 19:01:28 +09:00
takezoe
6dd1299dff (refs #2)Experimental implementation of forking repository. 2013-07-11 18:49:03 +09:00
takezoe
5e1eb39b87 (refs #2)Experimental implementation of forking repository. 2013-07-11 14:39:25 +09:00
137 changed files with 5660 additions and 1579 deletions

View File

@@ -6,20 +6,25 @@ GitBucket is the easily installable Github clone written with Scala.
The current version of GitBucket provides a basic features below:
- Public / Private Git repository (http access only)
- Repository viewer (some advanced features are not implemented)
- Repository viewer (some advanced features such as online file editing are not implemented)
- Repository search (Code and Issues)
- Wiki
- Issues
- Fork / Pull request
- Mail notification
- Activity timeline
- User management (for Administrators)
- Group (like Organization in Github)
- LDAP integration
- Gravatar support
Following features are not implemented, but we will make them in the future release!
- Fork and pull request
- Search
- File editing in repository viewer
- Comment for the changeset
- Network graph
- Statics
- Watch / Star
- Team management (like Organization in Github)
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki).
@@ -32,10 +37,33 @@ Installation
The default administrator account is **root** and password is **root**.
To upgrade GitBucket, only replace gitbucket.war.
(Since 1.6) or you can start GitBucket by ```java -jar gitbucket.war``` without servlet container. In this case, GitBucket URL is **http://[hostname]:8080/**. You can specify following options.
- --port=[NUMBER]
- --prefix=[CONTEXTPATH]
To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk.
Release Notes
--------
### 1.6 - COMMING SOON!
- Web hook.
- Performance improvement for pull request.
- Executable war file.
- Specify suitable Content-Type for downloaded files in the repository viewer.
- Fixed some bugs.
### 1.5 - 4 Sep 2013
- Fork and pull request.
- LDAP authentication.
- Mail notification.
- Add an option to turn off the gravatar support.
- Add the branch tab in the repository viewer.
- Encoding auto detection for the file content in the repository viewer.
- Add favicon, header logo and icons for the timeline.
- Specify data directory via environment variable GITBUCKET_HOME.
- Fixed some bugs.
### 1.4 - 31 Jul 2013
- Group management.
- Repository search for code and issues.

55
build.xml Normal file
View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" ?>
<project name="gitbucket" default="all" basedir=".">
<property name="target.dir" value="target"/>
<property name="embed.classes.dir" value="${target.dir}/embed-classes"/>
<property name="jetty.dir" value="embed-jetty"/>
<property name="scala.version" value="2.10"/>
<property name="gitbucket.version" value="0.0.1"/>
<property name="jetty.version" value="8.1.8.v20121106"/>
<property name="servlet.version" value="3.0.0.v201112011016"/>
<target name="clean">
<delete dir="${embed.classes.dir}"/>
<delete file="${target.dir}/scala-${scala.version}/gitbucket.war"/>
</target>
<target name="war" depends="clean">
<exec command="sbt.bat clean package"/>
</target>
<target name="embed" depends="war">
<mkdir dir="${embed.classes.dir}"/>
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/javax.servlet-${servlet.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-continuation-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-http-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-io-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-security-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-server-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-servlet-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-util-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-webapp-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-xml-${jetty.version}.jar" />
<zip destfile="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
basedir="${embed.classes.dir}"
update = "true"
includes="javax/**,org/**"/>
<zip destfile="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
basedir="${target.dir}/scala-${scala.version}/classes"
update = "true"
includes="JettyLauncher.class"/>
</target>
<target name="rename" depends="embed">
<rename src="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
dest="${target.dir}/scala-${scala.version}/gitbucket.war"/>
</target>
<target name="all" depends="rename">
</target>
</project>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -23,8 +23,8 @@
<constraint>
<height>-1</height>
<width>-1</width>
<x>37</x>
<y>36</y>
<x>33</x>
<y>18</y>
</constraint>
<sourceConnections/>
<targetConnections>
@@ -51,8 +51,8 @@
<constraint>
<height>-1</height>
<width>-1</width>
<x>751</x>
<y>47</y>
<x>723</x>
<y>138</y>
</constraint>
<sourceConnections>
<net.java.amateras.db.visual.model.ForeignKeyModel>
@@ -79,8 +79,8 @@
<constraint>
<height>-1</height>
<width>-1</width>
<x>882</x>
<y>239</y>
<x>1182</x>
<y>339</y>
</constraint>
<sourceConnections/>
<targetConnections>
@@ -108,8 +108,8 @@
<constraint>
<height>-1</height>
<width>-1</width>
<x>940</x>
<y>615</y>
<x>1301</x>
<y>836</y>
</constraint>
<sourceConnections>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
@@ -138,8 +138,8 @@
<constraint>
<height>-1</height>
<width>-1</width>
<x>420</x>
<y>758</y>
<x>684</x>
<y>858</y>
</constraint>
<sourceConnections>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
@@ -167,8 +167,8 @@
<constraint>
<height>-1</height>
<width>-1</width>
<x>307</x>
<y>356</y>
<x>293</x>
<y>478</y>
</constraint>
<sourceConnections>
<net.java.amateras.db.visual.model.ForeignKeyModel>
@@ -210,8 +210,8 @@
<constraint>
<height>-1</height>
<width>-1</width>
<x>641</x>
<y>569</y>
<x>875</x>
<y>677</y>
</constraint>
<sourceConnections>
<net.java.amateras.db.visual.model.ForeignKeyModel>
@@ -283,9 +283,14 @@
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>MILESTONE_NAME</columnName>
<logicalName>Milestone Name</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel/columnType"/>
<columnName>TITLE</columnName>
<logicalName>Title</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType">
<name>VARCHAR</name>
<logicalName>文字列</logicalName>
<supportSize>true</supportSize>
<type>12</type>
</columnType>
<size>100</size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
@@ -293,6 +298,49 @@
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>DESCRIPTION</columnName>
<logicalName>Description</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType">
<name>TEXT</name>
<logicalName>文字列</logicalName>
<supportSize>true</supportSize>
<type>2005</type>
</columnType>
<size></size>
<notNull>false</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>DUE_DATE</columnName>
<logicalName>Due Date</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType">
<name>TIMESTAMP</name>
<logicalName>日時</logicalName>
<supportSize>false</supportSize>
<type>93</type>
</columnType>
<size>10</size>
<notNull>false</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>CLOSED_DATE</columnName>
<logicalName>Closed Date</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel[6]/columnType"/>
<size>10</size>
<notNull>false</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
</columns>
<indices/>
<backgroundColor>
@@ -350,6 +398,36 @@
</entry>
</references>
</net.java.amateras.db.visual.model.ForeignKeyModel>
<net.java.amateras.db.visual.model.ForeignKeyModel>
<listeners serialization="custom">
<java.beans.PropertyChangeSupport>
<default>
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
</default>
<null/>
</java.beans.PropertyChangeSupport>
</listeners>
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../../../../../../../../../../../../../../../../.."/>
<foreignKeyName>ISSUE_FK_2</foreignKeyName>
<references>
<entry>
<net.java.amateras.db.visual.model.ColumnModel reference="../../../../net.java.amateras.db.visual.model.ForeignKeyModel[3]/references/entry/net.java.amateras.db.visual.model.ColumnModel"/>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>ASSIGNED_USER_NAME</columnName>
<logicalName>Assinged User Name</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
<size>100</size>
<notNull>false</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
</entry>
</references>
</net.java.amateras.db.visual.model.ForeignKeyModel>
</sourceConnections>
<targetConnections>
<net.java.amateras.db.visual.model.ForeignKeyModel>
@@ -375,8 +453,8 @@
<constraint>
<height>-1</height>
<width>-1</width>
<x>26</x>
<y>660</y>
<x>18</x>
<y>776</y>
</constraint>
<sourceConnections>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
@@ -462,6 +540,22 @@
<autoIncrement>true</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>ACTION</columnName>
<logicalName>Action</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType">
<name>VARCHAR</name>
<logicalName>文字列</logicalName>
<supportSize>true</supportSize>
<type>12</type>
</columnType>
<size>20</size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
<description>Expand to VARCHAR(20) from VARCHAR(10) in 1.3</description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel reference="../../sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/references/entry/net.java.amateras.db.visual.model.ColumnModel[2]"/>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>CONTENT</columnName>
@@ -498,7 +592,7 @@
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>UPDATED_DATE</columnName>
<logicalName>Updated Date</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel[7]/columnType"/>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel[8]/columnType"/>
<size>10</size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
@@ -572,10 +666,11 @@
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel reference="../../sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[4]/references/entry/net.java.amateras.db.visual.model.ColumnModel[2]"/>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>TITLE</columnName>
<logicalName>Title</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[6]/columnType"/>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[7]/columnType"/>
<size></size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
@@ -586,7 +681,7 @@
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>CONTENT</columnName>
<logicalName>Content</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[6]/columnType"/>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[7]/columnType"/>
<size></size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
@@ -597,7 +692,7 @@
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>REGISTERED_DATE</columnName>
<logicalName>Registered Date</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[7]/columnType"/>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[8]/columnType"/>
<size>10</size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
@@ -608,7 +703,7 @@
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>UPDATED_DATE</columnName>
<logicalName>Updated Date</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[7]/columnType"/>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[8]/columnType"/>
<size>10</size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
@@ -801,8 +896,8 @@
<constraint>
<height>-1</height>
<width>-1</width>
<x>388</x>
<y>166</y>
<x>481</x>
<y>361</y>
</constraint>
<sourceConnections>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
@@ -862,6 +957,250 @@
</net.java.amateras.db.visual.model.ForeignKeyModel>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel"/>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel"/>
<net.java.amateras.db.visual.model.ForeignKeyModel>
<listeners serialization="custom">
<java.beans.PropertyChangeSupport>
<default>
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
</default>
<null/>
</java.beans.PropertyChangeSupport>
</listeners>
<source class="net.java.amateras.db.visual.model.TableModel">
<listeners serialization="custom">
<java.beans.PropertyChangeSupport>
<default>
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../../.."/>
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
</default>
<null/>
</java.beans.PropertyChangeSupport>
</listeners>
<constraint>
<height>-1</height>
<width>-1</width>
<x>1199</x>
<y>25</y>
</constraint>
<sourceConnections>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
<net.java.amateras.db.visual.model.ForeignKeyModel>
<listeners serialization="custom">
<java.beans.PropertyChangeSupport>
<default>
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
</default>
<null/>
</java.beans.PropertyChangeSupport>
</listeners>
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../../../../../../../../../../.."/>
<foreignKeyName>ACTIVITY_FK_2</foreignKeyName>
<references>
<entry>
<net.java.amateras.db.visual.model.ColumnModel reference="../../../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[3]/references/entry/net.java.amateras.db.visual.model.ColumnModel"/>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>ACTIVITY_USER_NAME</columnName>
<logicalName>Activity User Name</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
<size>100</size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
</entry>
</references>
</net.java.amateras.db.visual.model.ForeignKeyModel>
</sourceConnections>
<targetConnections/>
<error></error>
<linkedPath></linkedPath>
<tableName>ACTIVITY</tableName>
<logicalName>Activity</logicalName>
<description>Since 1.2</description>
<columns>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>ACTIVITY_ID</columnName>
<logicalName>Activity ID</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType">
<name>INT</name>
<logicalName>整数</logicalName>
<supportSize>false</supportSize>
<type>4</type>
</columnType>
<size>10</size>
<notNull>true</notNull>
<primaryKey>true</primaryKey>
<description></description>
<autoIncrement>true</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>USER_NAME</columnName>
<logicalName>User Name</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
<size>100</size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>REPOSITORY_NAME</columnName>
<logicalName>Repository Name</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
<size>100</size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel reference="../../sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/references/entry/net.java.amateras.db.visual.model.ColumnModel[2]"/>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>ACTIVITY_TYPE</columnName>
<logicalName>Activity Type</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
<size>100</size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>MESSAGE</columnName>
<logicalName>Message</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[5]/columnType"/>
<size></size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>ADDITIONAL_INFO</columnName>
<logicalName>Additional Information</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[5]/columnType"/>
<size></size>
<notNull>false</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>ACTIVITY_DATE</columnName>
<logicalName>Activity Date</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[6]/columnType"/>
<size>10</size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
</columns>
<indices/>
<backgroundColor>
<red>255</red>
<green>255</green>
<blue>206</blue>
</backgroundColor>
<sql></sql>
</source>
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
<foreignKeyName>ACTIVITY_FK_1</foreignKeyName>
<references/>
</net.java.amateras.db.visual.model.ForeignKeyModel>
<net.java.amateras.db.visual.model.ForeignKeyModel>
<listeners serialization="custom">
<java.beans.PropertyChangeSupport>
<default>
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
</default>
<null/>
</java.beans.PropertyChangeSupport>
</listeners>
<source class="net.java.amateras.db.visual.model.TableModel">
<listeners serialization="custom">
<java.beans.PropertyChangeSupport>
<default>
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../../.."/>
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
</default>
<null/>
</java.beans.PropertyChangeSupport>
</listeners>
<constraint>
<height>-1</height>
<width>-1</width>
<x>1451</x>
<y>577</y>
</constraint>
<sourceConnections>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
</sourceConnections>
<targetConnections/>
<error></error>
<linkedPath></linkedPath>
<tableName>COMMIT_LOG</tableName>
<logicalName>Commit Log</logicalName>
<description>Since 1.2</description>
<columns>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>USER_NAME</columnName>
<logicalName>User Name</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
<size>100</size>
<notNull>true</notNull>
<primaryKey>true</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>REPOSITORY_NAME</columnName>
<logicalName>Repository Name</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
<size>100</size>
<notNull>true</notNull>
<primaryKey>true</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>COMMIT_ID</columnName>
<logicalName>Commit ID</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
<size>40</size>
<notNull>true</notNull>
<primaryKey>true</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
</columns>
<indices/>
<backgroundColor>
<red>255</red>
<green>255</green>
<blue>206</blue>
</backgroundColor>
<sql></sql>
</source>
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
<foreignKeyName>COMMIT_LOG_FK_1</foreignKeyName>
<references/>
</net.java.amateras.db.visual.model.ForeignKeyModel>
</targetConnections>
<error></error>
<linkedPath></linkedPath>
@@ -1062,6 +1401,100 @@
</net.java.amateras.db.visual.model.ForeignKeyModel>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[3]"/>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]"/>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[6]/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]"/>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[4]"/>
<net.java.amateras.db.visual.model.ForeignKeyModel>
<listeners serialization="custom">
<java.beans.PropertyChangeSupport>
<default>
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
</default>
<null/>
</java.beans.PropertyChangeSupport>
</listeners>
<source class="net.java.amateras.db.visual.model.TableModel">
<listeners serialization="custom">
<java.beans.PropertyChangeSupport>
<default>
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../../.."/>
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
</default>
<null/>
</java.beans.PropertyChangeSupport>
</listeners>
<constraint>
<height>-1</height>
<width>-1</width>
<x>432</x>
<y>240</y>
</constraint>
<sourceConnections>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
<net.java.amateras.db.visual.model.ForeignKeyModel>
<listeners serialization="custom">
<java.beans.PropertyChangeSupport>
<default>
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
</default>
<null/>
</java.beans.PropertyChangeSupport>
</listeners>
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../../../../.."/>
<foreignKeyName>GROUP_MEMBER_FK_2</foreignKeyName>
<references/>
</net.java.amateras.db.visual.model.ForeignKeyModel>
</sourceConnections>
<targetConnections/>
<error></error>
<linkedPath></linkedPath>
<tableName>GROUP_MEMBER</tableName>
<logicalName>Group Member</logicalName>
<description>Since 1.4</description>
<columns>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>GROUP_NAME</columnName>
<logicalName>Group Name</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
<size>100</size>
<notNull>true</notNull>
<primaryKey>true</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>USER_NAME</columnName>
<logicalName>User Name</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
<size>100</size>
<notNull>true</notNull>
<primaryKey>true</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
</columns>
<indices/>
<backgroundColor>
<red>255</red>
<green>255</green>
<blue>206</blue>
</backgroundColor>
<sql></sql>
</source>
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
<foreignKeyName>GROUP_MEMBER_FK_1</foreignKeyName>
<references>
<entry>
<net.java.amateras.db.visual.model.ColumnModel reference="../../../../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[3]/references/entry/net.java.amateras.db.visual.model.ColumnModel"/>
<net.java.amateras.db.visual.model.ColumnModel reference="../../../source/columns/net.java.amateras.db.visual.model.ColumnModel"/>
</entry>
</references>
</net.java.amateras.db.visual.model.ForeignKeyModel>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel[6]/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]"/>
</targetConnections>
<error></error>
<linkedPath></linkedPath>
@@ -1089,8 +1522,8 @@
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>PASSWORD</columnName>
<logicalName>Password</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel[2]/columnType"/>
<size>20</size>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
<size>40</size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
<description></description>
@@ -1098,18 +1531,18 @@
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>USER_TYPE</columnName>
<logicalName>User Type</logicalName>
<columnName>ADMINISTRATOR</columnName>
<logicalName>Administrator</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType">
<name>INT</name>
<logicalName>整数</logicalName>
<name>BOOLEAN</name>
<logicalName>真偽値</logicalName>
<supportSize>false</supportSize>
<type>4</type>
<type>16</type>
</columnType>
<size>10</size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
<description>0:Normal 1:Administrator</description>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue>0</defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
@@ -1157,6 +1590,33 @@
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>IMAGE</columnName>
<logicalName>Image</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[5]/columnType"/>
<size>100</size>
<notNull>false</notNull>
<primaryKey>false</primaryKey>
<description>Since 1.3</description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>GROUP_ACCOUNT</columnName>
<logicalName>Group Account</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType">
<name>BOOLEAN</name>
<logicalName>真偽値</logicalName>
<supportSize>false</supportSize>
<type>16</type>
</columnType>
<size>10</size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
<description>Since 1.4</description>
<autoIncrement>false</autoIncrement>
<defaultValue>FALSE</defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
</columns>
<indices>
<net.java.amateras.db.visual.model.IndexModel>
@@ -1184,6 +1644,91 @@
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target"/>
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source"/>
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[3]/source"/>
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[6]/source"/>
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[7]/source"/>
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[6]/source"/>
<net.java.amateras.db.visual.model.TableModel>
<listeners serialization="custom">
<java.beans.PropertyChangeSupport>
<default>
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../../.."/>
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
</default>
<null/>
</java.beans.PropertyChangeSupport>
</listeners>
<constraint>
<height>-1</height>
<width>-1</width>
<x>410</x>
<y>860</y>
</constraint>
<sourceConnections/>
<targetConnections/>
<error></error>
<linkedPath></linkedPath>
<tableName>ISSUE_OUTLINE_VIEW</tableName>
<logicalName>Issue Outline View</logicalName>
<description>Since 1.4</description>
<columns>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>USER_NAME</columnName>
<logicalName>User Name</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[5]/columnType"/>
<size>100</size>
<notNull>false</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>REPOSITORY_NAME</columnName>
<logicalName>Repository Name</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[5]/columnType"/>
<size>100</size>
<notNull>false</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>ISSUE_ID</columnName>
<logicalName>Issue ID</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType">
<name>INT</name>
<logicalName>整数</logicalName>
<supportSize>false</supportSize>
<type>4</type>
</columnType>
<size>10</size>
<notNull>false</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>COMMENT_COUNT</columnName>
<logicalName>Comment Count</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel[3]/columnType"/>
<size>10</size>
<notNull>false</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
</columns>
<indices/>
<backgroundColor>
<red>210</red>
<green>232</green>
<blue>249</blue>
</backgroundColor>
<sql></sql>
</net.java.amateras.db.visual.model.TableModel>
</children>
<dommains/>
<dialectName>H2</dialectName>

751
etc/icons.svg Normal file
View File

@@ -0,0 +1,751 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="744.09448819"
height="1052.3622047"
id="svg2"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="icons.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.4"
inkscape:cx="629.30023"
inkscape:cy="281.44758"
inkscape:document-units="px"
inkscape:current-layer="layer1-9"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="705"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:snap-global="true"
inkscape:snap-grids="false"
inkscape:snap-page="false"
inkscape:snap-bbox="true"
inkscape:bbox-paths="false"
inkscape:bbox-nodes="false"
inkscape:snap-to-guides="true">
<inkscape:grid
type="xygrid"
id="grid3080" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="layer1-9"
inkscape:label="Layer 1"
transform="matrix(0.66004549,0,0,0.66004549,12.445368,29.409765)">
<path
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:1.51504707px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 865.73247,686.51304 c 0,0 19.28074,14.1795 55.09542,13.7739 35.81468,-0.4056 45.91286,-13.7739 45.91286,-13.7739 l 31.84606,-118.8515 -163.46293,0 z"
id="path4000"
inkscape:connector-curvature="0"
sodipodi:nodetypes="czcccc" />
<path
style="fill:none;stroke:#b3b3b3;stroke-width:25.84518814;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 306.9072,1201.5096 c 0,0 3.44333,-28.5633 47.63498,-35.4849 15.10377,-2.3655 48.7968,-8.2798 48.7968,-42.5816"
id="path3207"
inkscape:connector-curvature="0"
inkscape:transform-center-x="-6.1348784"
sodipodi:nodetypes="csc"
inkscape:transform-center-y="1.9434039e-005" />
<path
style="fill:none;stroke:#b3b3b3;stroke-width:26.60422707;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 76.384718,1086.1545 c 0,82.8617 105.181182,77.9295 105.181182,77.9295"
id="path4318"
inkscape:connector-curvature="0" />
<rect
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:1.18291342;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
id="rect3935"
width="266.2222"
height="35.127476"
x="-4.6761055"
y="865.6405" />
<path
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:11.34059906;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 664.11762,675.10023 -74.94096,87.54344 20.17642,-92.15099 z"
id="path3894-1"
inkscape:connector-curvature="0" />
<rect
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:28.84111404;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
id="rect3088-5-5"
width="169.03172"
height="105.81662"
x="547.64557"
y="573.36456" />
<path
inkscape:connector-curvature="0"
id="path3850"
d="m 445.03908,191.42833 0,-128.577242 c 0,0 1.85983,-15.30681 -16.73849,-15.30681 -18.59831,0 -51.14538,0 -51.14538,0"
style="fill:none;stroke:#008000;stroke-width:22.7257061;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
id="path2991"
transform="translate(-137.57539,-163.64471)"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#008000;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc" />
<path
id="path2993"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc"
transform="matrix(0.83611704,0,0,0.83611704,-94.824045,-115.22257)" />
<rect
id="rect2995"
y="54.447956"
x="104.3765"
height="99.221695"
width="29.189819"
style="fill:#008000;stroke:#ffffff;stroke-width:1.11112404" />
<rect
id="rect2997"
y="173.24185"
x="104.63474"
height="26.258072"
width="29.724136"
style="fill:#008000;stroke:#ffffff;stroke-width:0.57680577" />
<rect
y="68.361099"
x="330.18893"
height="104.27071"
width="3.2554624"
id="rect3818"
style="fill:#ffffff;stroke:#008000;stroke-width:22.7257061;stroke-miterlimit:4;stroke-dasharray:none" />
<path
transform="matrix(1.0049237,0,0,0.61497516,-20.394061,56.890898)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4"
style="fill:#ffffff;stroke:#008000;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,-21.929587,-93.432709)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795"
style="fill:#ffffff;stroke:#008000;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,92.394578,56.992418)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-0"
style="fill:#ffffff;stroke:#008000;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
inkscape:connector-curvature="0"
id="path3852"
d="m 404.75446,10.803052 0,70.691447 L 359.1655,49.35988 z"
style="fill:#008000;stroke:#008000;stroke-width:0.83335358px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path3850-4"
d="m 448.69288,446.18012 0,-128.57725 c 0,0 1.85984,-15.30681 -16.73848,-15.30681 -18.59831,0 -51.14539,0 -51.14539,0"
style="fill:none;stroke:#800000;stroke-width:22.7257061;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
id="path2991-8"
transform="translate(-133.92158,91.107081)"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#800000;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc" />
<path
id="path2993-8"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc"
transform="matrix(0.83611704,0,0,0.83611704,-91.170233,139.52922)" />
<rect
id="rect2995-2"
y="309.19974"
x="108.03028"
height="99.221687"
width="29.189819"
style="fill:#800000;stroke:#ffffff;stroke-width:1.11112404" />
<rect
id="rect2997-4"
y="427.99362"
x="108.28852"
height="26.258072"
width="29.724136"
style="fill:#800000;stroke:#ffffff;stroke-width:0.57680577" />
<rect
y="323.11288"
x="333.84274"
height="104.27072"
width="3.2554622"
id="rect3818-5"
style="fill:#ffffff;stroke:#800000;stroke-width:22.7257061;stroke-miterlimit:4;stroke-dasharray:none" />
<path
transform="matrix(1.0049237,0,0,0.61497516,-16.740254,311.64269)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-5"
style="fill:#ffffff;stroke:#800000;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,-18.275774,161.31908)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-1"
style="fill:#ffffff;stroke:#800000;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,96.048392,311.7442)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-0-7"
style="fill:#ffffff;stroke:#800000;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
inkscape:connector-curvature="0"
id="path3852-1"
d="m 408.40826,265.55484 0,70.69144 -45.58895,-32.13461 z"
style="fill:#800000;stroke:#800000;stroke-width:0.83335358px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<rect
style="fill:#cccccc"
id="rect2985"
width="308.26331"
height="308.26331"
x="647.59973"
y="19.593252" />
<path
sodipodi:type="arc"
style="fill:#ffffff"
id="path2989"
sodipodi:cx="246.42857"
sodipodi:cy="327.36218"
sodipodi:rx="35"
sodipodi:ry="35"
d="m 281.42857,327.36218 c 0,19.32997 -15.67003,35 -35,35 -19.32996,0 -35,-15.67003 -35,-35 0,-19.32996 15.67004,-35 35,-35 19.32997,0 35,15.67004 35,35 z"
transform="matrix(2.9255147,0,0,2.9255147,83.281176,-813.70029)" />
<path
style="fill:#ffffff;stroke:#ffffff;stroke-width:1.59620917px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 715.46559,327.54005 179.96463,0 -89.85466,-201.67002 z"
id="path2993-2"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
inkscape:connector-curvature="0"
id="path3850-1"
d="m 447.16245,696.53224 0,-128.57724 c 0,0 1.85984,-15.30681 -16.73848,-15.30681 -18.59831,0 -51.14539,0 -51.14539,0"
style="fill:none;stroke:#b3b3b3;stroke-width:22.7257061;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
id="path2991-7"
transform="translate(-135.45201,341.45921)"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#b3b3b3;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc" />
<path
id="path2993-4"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc"
transform="matrix(0.83611704,0,0,0.83611704,-92.700665,389.88135)" />
<rect
id="rect2995-0"
y="559.55188"
x="106.49989"
height="99.221687"
width="29.189819"
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:1.11112404" />
<rect
id="rect2997-9"
y="678.34576"
x="106.75813"
height="26.258072"
width="29.724136"
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:0.57680577" />
<rect
y="573.46503"
x="332.31235"
height="104.27072"
width="3.2554622"
id="rect3818-4"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:22.7257061;stroke-miterlimit:4;stroke-dasharray:none" />
<path
transform="matrix(1.0049237,0,0,0.61497516,-18.270676,561.99481)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-8"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,-19.806206,411.67121)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-8"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,94.517962,562.09633)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-0-2"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
inkscape:connector-curvature="0"
id="path3852-4"
d="m 406.87783,515.90696 0,70.69145 -45.58895,-32.13462 z"
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:0.83335358px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<rect
style="fill:#ffffff;stroke:#ffffff;stroke-width:32.11899948;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
id="rect3088"
width="188.24272"
height="117.84301"
x="578.56567"
y="534.50873" />
<path
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:11.66586208;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 667.1767,647.76042 746.75901,734.99486 725.333,643.16913 z"
id="path3894"
inkscape:connector-curvature="0" />
<rect
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:28.84111404;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
id="rect3088-5"
width="169.03172"
height="105.81661"
x="595.6264"
y="533.38885" />
<path
id="path2991-7-7"
transform="matrix(0.81013086,0,0,0.81013086,-79.003905,648.21364)"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#b3b3b3;fill-rule:evenodd;stroke:#b3b3b3;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc" />
<path
id="path2993-4-1"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc"
transform="matrix(0.56153831,0,0,0.56153831,-15.312437,720.57846)" />
<path
id="path2991-7-1"
transform="translate(167.79377,599.09604)"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#b3b3b3;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc" />
<path
id="path2993-4-5"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc"
transform="matrix(0.83611704,0,0,0.83611704,210.54515,647.51817)" />
<rect
id="rect2995-0-2"
y="817.18872"
x="409.74567"
height="99.221687"
width="29.189819"
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:1.11112404" />
<rect
id="rect2997-9-7"
y="935.98169"
x="410.00391"
height="26.258072"
width="29.724136"
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:0.57680577" />
<path
style="fill:#ffffff;stroke:#ffffff;stroke-width:2.92446065;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
d="m 383.15829,850.33665 -64.6851,-36.2114 10.70013,55.95688 53.98497,-19.74548 z"
id="rect4046-3"
inkscape:connector-curvature="0" />
<path
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:1.98877633;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
d="m 372.50197,843.46474 -43.65605,-24.43447 6.99871,38.15621 36.65734,-13.72174 z"
id="rect4046"
inkscape:connector-curvature="0" />
<path
style="fill:#ffffff;stroke:#ffffff;stroke-width:2.92446065;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
d="m 462.88559,934.94792 64.6851,36.21128 -10.70013,-55.95672 -53.98497,19.74544 z"
id="rect4046-3-2"
inkscape:connector-curvature="0" />
<path
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:1.98877633;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
d="m 471.91864,943.98419 43.65605,24.43442 -6.99871,-38.15607 -36.65734,13.72165 z"
id="rect4046-1"
inkscape:connector-curvature="0" />
<path
id="path2991-7-79"
transform="translate(439.9024,596.03518)"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#b3b3b3;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc" />
<path
id="path2993-4-54"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc"
transform="matrix(0.83611704,0,0,0.83611704,482.65378,644.45731)" />
<rect
style="fill:#ffffff;stroke:#ffffff;stroke-width:7.27556181;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
id="rect4271"
width="55.131588"
height="89.475853"
x="1123.0723"
y="8.2489862"
transform="matrix(0.69198127,0.72191545,-0.69198127,0.72191545,0,0)" />
<rect
id="rect2995-0-3-3"
y="1106.4344"
x="-89.869194"
height="57.711208"
width="24.529409"
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:0.77681416"
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)" />
<rect
id="rect2995-0-3-2"
y="7.221128"
x="1139.5251"
height="82.866272"
width="24.378254"
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:0.92796957"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
<rect
id="rect2995-0-3"
y="814.12781"
x="681.85431"
height="99.221687"
width="29.189819"
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:1.11112404" />
<rect
id="rect2997-9-1"
y="932.58148"
x="682.54327"
height="26.258072"
width="29.724136"
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:0.57680577" />
<rect
y="1088.6628"
x="76.264809"
height="104.27072"
width="3.2554622"
id="rect3818-4-8"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:22.7257061;stroke-miterlimit:4;stroke-dasharray:none" />
<path
transform="matrix(1.0049237,0,0,0.61497516,-274.3181,1077.1951)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-8-7"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,-275.85363,926.87175)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-8-4"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,-161.78913,1021.9512)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-8-7-7"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<rect
y="1087.278"
x="304.77451"
height="104.27072"
width="3.2554622"
id="rect3818-4-8-4"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:22.7257061;stroke-miterlimit:4;stroke-dasharray:none" />
<path
transform="matrix(1.0049237,0,0,0.61497516,-45.808546,1075.8101)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-8-7-8"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,-47.344075,925.48675)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-8-4-8"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,53.509086,972.5163)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-8-4-8-2"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
sodipodi:type="arc"
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:6.68107271;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
id="path3992-4"
sodipodi:cx="490.42908"
sodipodi:cy="950.84186"
sodipodi:rx="18.487062"
sodipodi:ry="26.506598"
d="m 508.91614,950.84186 c 0,14.63919 -8.27694,26.5066 -18.48706,26.5066 -10.21013,0 -18.48707,-11.86741 -18.48707,-26.5066 0,-14.63919 8.27694,-26.5066 18.48707,-26.5066 10.21012,0 18.48706,11.86741 18.48706,26.5066 z"
transform="matrix(4.8923198,0,0,1.0737805,-1482.0573,-466.94845)" />
<path
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:12.98546886;stroke-miterlimit:4;stroke-dasharray:none"
d="m 967.57233,525.26244 0,30.6797 -28.50183,0 0,41.2377 28.50183,0 0,27.1288 41.19027,0 0,-27.1288 29.354,0 0,-41.2377 -29.354,0 0,-30.6797 -41.19027,0 z"
id="rect2995-0-2-7"
inkscape:connector-curvature="0" />
<path
id="path2991-7-2"
transform="translate(717.27126,597.74227)"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#b3b3b3;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc" />
<path
id="path2993-4-7"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc"
transform="matrix(0.7638244,0,0,0.7638244,777.85958,666.54744)" />
<rect
id="rect2995-0-6"
y="-220.76018"
x="1298.3352"
height="189.71017"
width="28.775486"
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:1.52545774"
transform="matrix(0.67068946,0.74173826,-0.74173826,0.67068946,0,0)" />
<g
id="g4284"
transform="translate(-77.916708,-8.657412)">
<path
sodipodi:nodetypes="czcczcc"
inkscape:connector-curvature="0"
id="rect4201"
d="m 568.37427,1080.8464 c 0,0 55.60005,-9.5933 75.06243,-8.6574 19.46238,0.9359 40.43273,8.6574 40.43273,8.6574 l 0,141.4674 c 0,0 -20.97035,-7.7215 -40.43273,-8.6574 -19.46238,-0.9359 -75.06243,8.6574 -75.06243,8.6574 z"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:14.36538029;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4" />
<rect
y="1108.1473"
x="597.4068"
height="5.4857273"
width="55.265846"
id="rect4203"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:11.82844734;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
<rect
y="1142.7776"
x="598.48895"
height="5.4857273"
width="55.26585"
id="rect4203-2"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:11.82844734;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
<rect
y="1176.1093"
x="598.48895"
height="5.4857273"
width="55.26585"
id="rect4203-2-3"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:11.82844734;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
<path
sodipodi:nodetypes="czc"
inkscape:connector-curvature="0"
id="path4245"
d="m 563.55369,1233.6274 c 0,0 59.11965,-16.1473 81.00954,-14.7566 21.8899,1.3907 46.29117,14.7566 46.29117,14.7566"
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:19.6372261;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<g
transform="matrix(-1.0032405,0,0,1,1329.8708,99.560238)"
id="g4277">
<path
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:14.36538124;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4"
d="m 519.67634,980.83663 c 0,0 55.60005,-9.5933 75.06243,-8.6574 19.46238,0.9359 40.43272,8.6574 40.43272,8.6574 l 0,141.46737 c 0,0 -20.97034,-7.7215 -40.43272,-8.6574 -19.46238,-0.9359 -75.06243,8.6574 -75.06243,8.6574 z"
id="rect4201-2"
inkscape:connector-curvature="0"
sodipodi:nodetypes="czcczcc" />
<rect
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:11.82844734;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
id="rect4203-21"
width="55.26585"
height="5.4857273"
x="548.70886"
y="1008.1376" />
<rect
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:11.82844734;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
id="rect4203-2-6"
width="55.26585"
height="5.4857273"
x="549.79102"
y="1042.7678" />
<rect
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:11.82844734;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
id="rect4203-2-3-8"
width="55.26585"
height="5.4857273"
x="549.79102"
y="1076.0995" />
<path
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:19.6372261;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 514.85576,1133.6176 c 0,0 59.11965,-16.1473 81.00954,-14.7566 21.8899,1.3907 46.29116,14.7566 46.29116,14.7566"
id="path4245-5"
inkscape:connector-curvature="0"
sodipodi:nodetypes="czc" />
</g>
</g>
<g
id="g3107"
transform="matrix(0.53086704,-0.53086704,0.53086704,0.53086704,-205.0028,934.47839)">
<rect
y="1165.7029"
x="793.91357"
height="177.36816"
width="131.91675"
id="rect3075"
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:19.58793259;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
<rect
transform="matrix(0.69911762,0.71500668,-0.71500668,0.69911762,0,0)"
y="145.59781"
x="1379.6274"
height="95.711494"
width="95.711456"
id="rect3075-1"
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:12.25645447;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
<path
transform="matrix(1.5150471,0,0,1.5150471,-201.2129,-64.133761)"
d="m 710,812.36218 c 0,5.52285 -4.47715,10 -10,10 -5.52285,0 -10,-4.47715 -10,-10 0,-5.52284 4.47715,-10 10,-10 5.52285,0 10,4.47716 10,10 z"
sodipodi:ry="10"
sodipodi:rx="10"
sodipodi:cy="812.36218"
sodipodi:cx="700"
id="path3100"
style="fill:#ffffff;stroke:#ffffff;stroke-width:12.69999981;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
</g>
<path
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:12.98546886;stroke-miterlimit:4;stroke-dasharray:none"
d="m 937.41093,1044.4944 0,30.6797 -28.50183,0 0,41.2377 28.50183,0 0,27.1288 41.19033,0 0,-27.1288 29.35404,0 0,-41.2377 -29.35404,0 0,-30.6797 -41.19033,0 z"
id="rect2995-0-2-7-7"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

View File

@@ -2,6 +2,7 @@ import sbt._
import Keys._
import org.scalatra.sbt._
import org.scalatra.sbt.PluginKeys._
import sbt.ScalaVersion
import twirl.sbt.TwirlPlugin._
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
@@ -10,7 +11,7 @@ object MyBuild extends Build {
val Name = "gitbucket"
val Version = "0.0.1"
val ScalaVersion = "2.10.1"
val ScalatraVersion = "2.2.0"
val ScalatraVersion = "2.2.1"
lazy val project = Project (
"gitbucket",
@@ -20,24 +21,31 @@ object MyBuild extends Build {
name := Name,
version := Version,
scalaVersion := ScalaVersion,
resolvers += Classpaths.typesafeReleases,
resolvers ++= Seq(
Classpaths.typesafeReleases,
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
),
libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r",
"org.apache.commons" % "commons-io" % "1.3.2",
"org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
"org.json4s" %% "json4s-jackson" % "3.2.4",
"jp.sf.amateras" %% "scalatra-forms" % "0.0.2",
"commons-io" % "commons-io" % "2.4",
"org.pegdown" % "pegdown" % "1.3.0",
"org.apache.commons" % "commons-compress" % "1.5",
"org.apache.commons" % "commons-email" % "1.3.1",
"org.apache.httpcomponents" % "httpclient" % "4.2.5",
"com.typesafe.slick" %% "slick" % "1.0.1",
"com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.3.171",
"ch.qos.logback" % "logback-classic" % "1.0.6" % "runtime",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar"))
),
EclipseKeys.withSource := true
EclipseKeys.withSource := true,
packageOptions += Package.MainClass("JettyLauncher")
) ++ seq(Twirl.settings: _*)
)
}

View File

@@ -1,7 +1,7 @@
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.1.2")
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.2.0")
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.2.0")
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.1")
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.2.0")
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.0")
addSbtPlugin("io.spray" % "sbt-twirl" % "0.6.1")

View File

@@ -1,2 +1,2 @@
set SCRIPT_DIR=%~dp0
java -Dhttp.proxyHost=proxy.intellilink.co.jp -Dhttp.proxyPort=8080 -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar "%SCRIPT_DIR%\sbt-launch-0.12.3.jar" %*
java -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar "%SCRIPT_DIR%\sbt-launch-0.12.3.jar" %*

1
sbt.sh Executable file
View File

@@ -0,0 +1 @@
java -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar `dirname $0`/sbt-launch-0.12.3.jar "$@"

View File

@@ -0,0 +1,39 @@
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;
import java.net.URL;
import java.security.ProtectionDomain;
public class JettyLauncher {
public static void main(String[] args) throws Exception {
int port = 8080;
String contextPath = "/";
for(String arg: args){
if(arg.startsWith("--") && arg.contains("=")){
String[] dim = arg.split("=");
if(dim.length >= 2){
if(dim[0].equals("--port")){
port = Integer.parseInt(dim[1]);
} else if(dim[0].equals("--prefix")){
contextPath = dim[1];
}
}
}
}
Server server = new Server(port);
WebAppContext context = new WebAppContext();
ProtectionDomain domain = JettyLauncher.class.getProtectionDomain();
URL location = domain.getCodeSource().getLocation();
context.setContextPath(contextPath);
context.setDescriptor(location.toExternalForm() + "/WEB-INF/web.xml");
context.setServer(server);
context.setWar(location.toExternalForm());
server.setHandler(context);
server.start();
server.join();
}
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 473 B

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,21 @@
ALTER TABLE REPOSITORY ADD COLUMN ORIGIN_USER_NAME VARCHAR(100);
ALTER TABLE REPOSITORY ADD COLUMN ORIGIN_REPOSITORY_NAME VARCHAR(100);
ALTER TABLE REPOSITORY ADD COLUMN PARENT_USER_NAME VARCHAR(100);
ALTER TABLE REPOSITORY ADD COLUMN PARENT_REPOSITORY_NAME VARCHAR(100);
CREATE TABLE PULL_REQUEST(
USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL,
ISSUE_ID INT NOT NULL,
BRANCH VARCHAR(100) NOT NULL,
REQUEST_USER_NAME VARCHAR(100) NOT NULL,
REQUEST_REPOSITORY_NAME VARCHAR(100) NOT NULL,
REQUEST_BRANCH VARCHAR(100) NOT NULL,
COMMIT_ID_FROM VARCHAR(40) NOT NULL,
COMMIT_ID_TO VARCHAR(40) NOT NULL
);
ALTER TABLE PULL_REQUEST ADD CONSTRAINT IDX_PULL_REQUEST_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
ALTER TABLE PULL_REQUEST ADD CONSTRAINT IDX_PULL_REQUEST_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
ALTER TABLE ISSUE ADD COLUMN PULL_REQUEST BOOLEAN NOT NULL DEFAULT FALSE;

View File

@@ -0,0 +1,8 @@
CREATE TABLE WEB_HOOK (
USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL,
URL VARCHAR(200) NOT NULL
);
ALTER TABLE WEB_HOOK ADD CONSTRAINT IDX_WEB_HOOK_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, URL);
ALTER TABLE WEB_HOOK ADD CONSTRAINT IDX_WEB_HOOK_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);

View File

@@ -18,6 +18,7 @@ class ScalatraBootstrap extends LifeCycle {
context.mount(new LabelsController, "/*")
context.mount(new MilestonesController, "/*")
context.mount(new IssuesController, "/*")
context.mount(new PullRequestsController, "/*")
context.mount(new RepositorySettingsController, "/*")
val dir = new java.io.File(_root_.util.Directory.GitBucketHome)

View File

@@ -1,7 +1,9 @@
package app
import _root_.util.Directory._
import _root_.util.{StringUtil, FileUtil, Validations}
import _root_.util.Implicits._
import _root_.util.ControlUtil._
import _root_.util.{FileUtil, Validations, Keys}
import org.scalatra._
import org.scalatra.json._
import org.json4s._
@@ -22,6 +24,9 @@ abstract class ControllerBase extends ScalatraFilter
implicit val jsonFormats = DefaultFormats
// Don't set content type via Accept header.
override def format(implicit request: HttpServletRequest) = ""
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
val httpRequest = request.asInstanceOf[HttpServletRequest]
val httpResponse = response.asInstanceOf[HttpServletResponse]
@@ -29,7 +34,7 @@ abstract class ControllerBase extends ScalatraFilter
val path = httpRequest.getRequestURI.substring(context.length)
if(path.startsWith("/console/")){
val account = httpRequest.getSession.getAttribute("LOGIN_ACCOUNT").asInstanceOf[Account]
val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
if(account == null){
// Redirect to login form
httpResponse.sendRedirect(context + "/signin?" + path)
@@ -54,68 +59,59 @@ abstract class ControllerBase extends ScalatraFilter
*/
implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL, request)
private def currentURL: String = {
val queryString = request.getQueryString
private def currentURL: String = defining(request.getQueryString){ queryString =>
request.getRequestURI + (if(queryString != null) "?" + queryString else "")
}
private def LoginAccount: Option[Account] = {
session.get("LOGIN_ACCOUNT") match {
case Some(x: Account) => Some(x)
case _ => None
}
}
private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount)
def ajaxGet(path : String)(action : => Any) : Route = {
def ajaxGet(path : String)(action : => Any) : Route =
super.get(path){
request.setAttribute("AJAX", "true")
request.setAttribute(Keys.Request.Ajax, "true")
action
}
}
override def ajaxGet[T](path : String, form : MappingValueType[T])(action : T => Any) : Route = {
override def ajaxGet[T](path : String, form : MappingValueType[T])(action : T => Any) : Route =
super.ajaxGet(path, form){ form =>
request.setAttribute("AJAX", "true")
request.setAttribute(Keys.Request.Ajax, "true")
action(form)
}
}
def ajaxPost(path : String)(action : => Any) : Route = {
def ajaxPost(path : String)(action : => Any) : Route =
super.post(path){
request.setAttribute("AJAX", "true")
request.setAttribute(Keys.Request.Ajax, "true")
action
}
}
override def ajaxPost[T](path : String, form : MappingValueType[T])(action : T => Any) : Route = {
override def ajaxPost[T](path : String, form : MappingValueType[T])(action : T => Any) : Route =
super.ajaxPost(path, form){ form =>
request.setAttribute("AJAX", "true")
request.setAttribute(Keys.Request.Ajax, "true")
action(form)
}
}
protected def NotFound() = {
if(request.getAttribute("AJAX") == null){
org.scalatra.NotFound(html.error("Not Found"))
} else {
protected def NotFound() =
if(request.hasAttribute(Keys.Request.Ajax)){
org.scalatra.NotFound()
} else {
org.scalatra.NotFound(html.error("Not Found"))
}
}
protected def Unauthorized()(implicit context: app.Context) = {
if(request.getAttribute("AJAX") == null){
protected def Unauthorized()(implicit context: app.Context) =
if(request.hasAttribute(Keys.Request.Ajax)){
org.scalatra.Unauthorized()
} else {
if(context.loginAccount.isDefined){
org.scalatra.Unauthorized(redirect("/"))
} else {
org.scalatra.Unauthorized(redirect("/signin?" + currentURL))
if(request.getMethod.toUpperCase == "POST"){
org.scalatra.Unauthorized(redirect("/signin"))
} else {
org.scalatra.Unauthorized(redirect("/signin?redirect=" + currentURL))
}
}
} else {
org.scalatra.Unauthorized()
}
}
protected def baseUrl = {
val url = request.getRequestURL.toString
protected def baseUrl = defining(request.getRequestURL.toString){ url =>
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
}
@@ -126,19 +122,26 @@ abstract class ControllerBase extends ScalatraFilter
*/
case class Context(path: String, loginAccount: Option[Account], currentUrl: String, request: HttpServletRequest){
def redirectUrl = if(request.getParameter("redirect") != null){
request.getParameter("redirect")
} else {
currentUrl
}
/**
* Get object from cache.
*
* If object has not been cached with the specified key then retrieves by given action.
* Cached object are available during a request.
*/
def cache[A](key: String)(action: => A): A = {
Option(request.getAttribute("cache." + key).asInstanceOf[A]).getOrElse {
val newObject = action
request.setAttribute("cache." + key, newObject)
newObject
def cache[A](key: String)(action: => A): A =
defining(Keys.Request.Cache(key)){ cacheKey =>
Option(request.getAttribute(cacheKey).asInstanceOf[A]).getOrElse {
val newObject = action
request.setAttribute(cacheKey, newObject)
newObject
}
}
}
}
@@ -148,7 +151,7 @@ case class Context(path: String, loginAccount: Option[Account], currentUrl: Stri
trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase {
self: AccountService =>
protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit = {
protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit =
if(clearImage){
getAccountByUserName(userName).flatMap(_.image).map { image =>
new java.io.File(getUserUploadDir(userName), image).delete()
@@ -164,15 +167,14 @@ trait AccountManagementControllerBase extends ControllerBase with FileUploadCont
updateAvatarImage(userName, Some(filename))
}
}
}
protected def uniqueUserName: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
override def validate(name: String, value: String): Option[String] =
getAccountByUserName(value).map { _ => "User already exists." }
}
protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
getAccountByMailAddress(value)
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
.map { _ => "Mail address is already registered." }
@@ -200,12 +202,7 @@ trait FileUploadControllerBase {
def removeTemporaryFiles()(implicit session: HttpSession): Unit =
FileUtils.deleteDirectory(TemporaryDir)
def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] = {
val filename = Option(session.getAttribute("upload_" + fileId).asInstanceOf[String])
if(filename.isDefined){
session.removeAttribute("upload_" + fileId)
}
filename
}
def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] =
session.getAndRemove[String](Keys.Session.Upload(fileId))
}

View File

@@ -1,27 +1,32 @@
package app
import util.Directory._
import util.{JGitUtil, UsersAuthenticator}
import util.ControlUtil._
import util._
import service._
import java.io.File
import org.eclipse.jgit.api.Git
import org.apache.commons.io._
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.lib.PersonIdent
import scala.Some
class CreateRepositoryController extends CreateRepositoryControllerBase
with RepositoryService with AccountService with WikiService with LabelsService with ActivityService
with UsersAuthenticator
with UsersAuthenticator with ReadableUsersAuthenticator
/**
* Creates new repository.
*/
trait CreateRepositoryControllerBase extends ControllerBase {
self: RepositoryService with AccountService with WikiService with LabelsService with ActivityService
with UsersAuthenticator =>
with UsersAuthenticator with ReadableUsersAuthenticator =>
case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
val form = mapping(
case class ForkRepositoryForm(owner: String, name: String)
val newForm = mapping(
"owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, unique))),
"description" -> trim(label("Description" , optional(text()))),
@@ -29,6 +34,11 @@ trait CreateRepositoryControllerBase extends ControllerBase {
"createReadme" -> trim(label("Create README" , boolean()))
)(RepositoryCreationForm.apply)
val forkForm = mapping(
"owner" -> trim(label("Repository owner", text(required))),
"name" -> trim(label("Repository name", text(required)))
)(ForkRepositoryForm.apply)
/**
* Show the new repository form.
*/
@@ -39,82 +49,147 @@ trait CreateRepositoryControllerBase extends ControllerBase {
/**
* Create new repository.
*/
post("/new", form)(usersOnly { form =>
val ownerAccount = getAccountByUserName(form.owner).get
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
post("/new", newForm)(usersOnly { form =>
LockUtil.lock(s"${form.owner}/${form.name}/create"){
if(getRepository(form.owner, form.name, baseUrl).isEmpty){
val ownerAccount = getAccountByUserName(form.owner).get
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
// Insert to the database at first
createRepository(form.name, form.owner, form.description, form.isPrivate)
// Insert to the database at first
createRepository(form.name, form.owner, form.description, form.isPrivate)
// Add collaborators for group repository
if(ownerAccount.isGroupAccount){
getGroupMembers(form.owner).foreach { userName =>
addCollaborator(form.owner, form.name, userName)
// Add collaborators for group repository
if(ownerAccount.isGroupAccount){
getGroupMembers(form.owner).foreach { userName =>
addCollaborator(form.owner, form.name, userName)
}
}
// Insert default labels
insertDefaultLabels(form.owner, form.name)
// Create the actual repository
val gitdir = getRepositoryDir(form.owner, form.name)
JGitUtil.initRepository(gitdir)
if(form.createReadme){
val tmpdir = getInitRepositoryDir(form.owner, form.name)
try {
// Clone the repository
Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call
// Create README.md
FileUtils.writeStringToFile(new File(tmpdir, "README.md"),
if(form.description.nonEmpty){
form.name + "\n" +
"===============\n" +
"\n" +
form.description.get
} else {
form.name + "\n" +
"===============\n"
}, "UTF-8")
val git = Git.open(tmpdir)
git.add.addFilepattern("README.md").call
git.commit
.setCommitter(new PersonIdent(loginUserName, loginAccount.mailAddress))
.setMessage("Initial commit").call
git.push.call
} finally {
FileUtils.deleteDirectory(tmpdir)
}
}
// Create Wiki repository
createWikiRepository(loginAccount, form.owner, form.name)
// Record activity
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
}
// redirect to the repository
redirect(s"/${form.owner}/${form.name}")
}
// Insert default labels
createLabel(form.owner, form.name, "bug", "fc2929")
createLabel(form.owner, form.name, "duplicate", "cccccc")
createLabel(form.owner, form.name, "enhancement", "84b6eb")
createLabel(form.owner, form.name, "invalid", "e6e6e6")
createLabel(form.owner, form.name, "question", "cc317c")
createLabel(form.owner, form.name, "wontfix", "ffffff")
// Create the actual repository
val gitdir = getRepositoryDir(form.owner, form.name)
JGitUtil.initRepository(gitdir)
if(form.createReadme){
val tmpdir = getInitRepositoryDir(form.owner, form.name)
try {
// Clone the repository
Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call
// Create README.md
FileUtils.writeStringToFile(new File(tmpdir, "README.md"),
if(form.description.nonEmpty){
form.name + "\n" +
"===============\n" +
"\n" +
form.description.get
} else {
form.name + "\n" +
"===============\n"
}, "UTF-8")
val git = Git.open(tmpdir)
git.add.addFilepattern("README.md").call
git.commit.setCommitter(loginAccount.userName, loginAccount.mailAddress).setMessage("Initial commit").call
git.push.call
} finally {
FileUtils.deleteDirectory(tmpdir)
}
}
// Create Wiki repository
createWikiRepository(loginAccount, form.owner, form.name)
// Record activity
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
// redirect to the repository
redirect(s"/${form.owner}/${form.name}")
})
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
LockUtil.lock(s"${loginUserName}/${repository.name}/create"){
if(getRepository(loginUserName, repository.name, baseUrl).isEmpty){
// Insert to the database at first
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
createRepository(
repositoryName = repository.name,
userName = loginUserName,
description = repository.repository.description,
isPrivate = repository.repository.isPrivate,
originRepositoryName = Some(originRepositoryName),
originUserName = Some(originUserName),
parentRepositoryName = Some(repository.name),
parentUserName = Some(repository.owner)
)
// Insert default labels
insertDefaultLabels(loginUserName, repository.name)
// clone repository actually
JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name),
getRepositoryDir(loginUserName, repository.name))
// Create Wiki repository
JGitUtil.cloneRepository(
getWikiRepositoryDir(repository.owner, repository.name),
getWikiRepositoryDir(loginUserName, repository.name))
// insert commit id
using(Git.open(getRepositoryDir(loginUserName, repository.name))){ git =>
JGitUtil.getRepositoryInfo(loginUserName, repository.name, baseUrl).branchList.foreach { branch =>
JGitUtil.getCommitLog(git, branch) match {
case Right((commits, _)) => commits.foreach { commit =>
if(!existsCommitId(loginUserName, repository.name, commit.id)){
insertCommitId(loginUserName, repository.name, commit.id)
}
}
case Left(_) => ???
}
}
}
// Record activity
recordForkActivity(repository.owner, repository.name, loginUserName)
}
// redirect to the repository
redirect("/%s/%s".format(loginUserName, repository.name))
}
})
private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
createLabel(userName, repositoryName, "bug", "fc2929")
createLabel(userName, repositoryName, "duplicate", "cccccc")
createLabel(userName, repositoryName, "enhancement", "84b6eb")
createLabel(userName, repositoryName, "invalid", "e6e6e6")
createLabel(userName, repositoryName, "question", "cc317c")
createLabel(userName, repositoryName, "wontfix", "ffffff")
}
private def existsAccount: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
override def validate(name: String, value: String): Option[String] =
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
}
/**
* Duplicate check for the repository name.
*/
private def unique: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
params.get("owner").flatMap { userName =>
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
}

View File

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

View File

@@ -1,6 +1,7 @@
package app
import util.{FileUtil}
import _root_.util.{Keys, FileUtil}
import util.ControlUtil._
import org.scalatra._
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport}
import org.apache.commons.io.FileUtils
@@ -18,10 +19,9 @@ class FileUploadController extends ScalatraServlet
post("/image"){
fileParams.get("file") match {
case Some(file) if(FileUtil.isImage(file.name)) => {
val fileId = generateFileId
case Some(file) if(FileUtil.isImage(file.name)) => defining(generateFileId){ fileId =>
FileUtils.writeByteArrayToFile(getTemporaryFile(fileId), file.get)
session += "upload_" + fileId -> file.name
session += Keys.Session.Upload(fileId) -> file.name
Ok(fileId)
}
case None => BadRequest

View File

@@ -4,7 +4,9 @@ import jp.sf.amateras.scalatra.forms._
import service._
import IssuesService._
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator}
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator, Notifier, Keys}
import util.Implicits._
import util.ControlUtil._
import org.scalatra.Ok
class IssuesController extends IssuesControllerBase
@@ -57,12 +59,9 @@ trait IssuesControllerBase extends ControllerBase {
})
get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
val owner = repository.owner
val name = repository.name
val issueId = params("id")
getIssue(owner, name, issueId) map {
issues.html.issue(
defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) =>
getIssue(owner, name, issueId) map {
issues.html.issue(
_,
getComments(owner, name, issueId.toInt),
getIssueLabels(owner, name, issueId.toInt),
@@ -71,84 +70,89 @@ trait IssuesControllerBase extends ControllerBase {
getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount),
repository)
} getOrElse NotFound
} getOrElse NotFound
}
})
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
val owner = repository.owner
val name = repository.name
issues.html.create(
(getCollaborators(owner, name) :+ owner).sorted,
getMilestones(owner, name),
getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount),
repository)
defining(repository.owner, repository.name){ case (owner, name) =>
issues.html.create(
(getCollaborators(owner, name) :+ owner).sorted,
getMilestones(owner, name),
getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount),
repository)
}
})
post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
val owner = repository.owner
val name = repository.name
val writable = hasWritePermission(owner, name, context.loginAccount)
val userName = context.loginAccount.get.userName
defining(repository.owner, repository.name){ case (owner, name) =>
val writable = hasWritePermission(owner, name, context.loginAccount)
val userName = context.loginAccount.get.userName
// insert issue
val issueId = createIssue(owner, name, userName, form.title, form.content,
if(writable) form.assignedUserName else None,
if(writable) form.milestoneId else None)
// insert issue
val issueId = createIssue(owner, name, userName, form.title, form.content,
if(writable) form.assignedUserName else None,
if(writable) form.milestoneId else None)
// insert labels
if(writable){
form.labelNames.map { value =>
val labels = getLabels(owner, name)
value.split(",").foreach { labelName =>
labels.find(_.labelName == labelName).map { label =>
registerIssueLabel(owner, name, issueId, label.labelId)
// insert labels
if(writable){
form.labelNames.map { value =>
val labels = getLabels(owner, name)
value.split(",").foreach { labelName =>
labels.find(_.labelName == labelName).map { label =>
registerIssueLabel(owner, name, issueId, label.labelId)
}
}
}
}
// record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
// notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgIssue(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
}
redirect(s"/${owner}/${name}/issues/${issueId}")
}
// record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
redirect(s"/${owner}/${name}/issues/${issueId}")
})
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
val owner = repository.owner
val name = repository.name
getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){
updateIssue(owner, name, issue.issueId, form.title, form.content)
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized
} getOrElse NotFound
defining(repository.owner, repository.name){ case (owner, name) =>
getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){
updateIssue(owner, name, issue.issueId, form.title, form.content)
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized
} getOrElse NotFound
}
})
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
handleComment(form.issueId, Some(form.content), repository)() map { id =>
redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}")
handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) =>
redirect(s"/${repository.owner}/${repository.name}/${
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
} getOrElse NotFound
})
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
handleComment(form.issueId, form.content, repository)() map { id =>
redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}")
handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
redirect(s"/${repository.owner}/${repository.name}/${
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
} getOrElse NotFound
})
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
val owner = repository.owner
val name = repository.name
getComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){
updateComment(comment.commentId, form.content)
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
} else Unauthorized
} getOrElse NotFound
defining(repository.owner, repository.name){ case (owner, name) =>
getComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){
updateComment(comment.commentId, form.content)
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
} else Unauthorized
} getOrElse NotFound
}
})
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
@@ -187,17 +191,17 @@ trait IssuesControllerBase extends ControllerBase {
})
ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
val issueId = params("id").toInt
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
defining(params("id").toInt){ issueId =>
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
}
})
ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository =>
val issueId = params("id").toInt
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
defining(params("id").toInt){ issueId =>
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
}
})
ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
@@ -216,36 +220,36 @@ trait IssuesControllerBase extends ControllerBase {
})
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
val action = params.get("value")
executeBatch(repository) {
handleComment(_, None, repository)( _ => action)
defining(params.get("value")){ action =>
executeBatch(repository) {
handleComment(_, None, repository)( _ => action)
}
}
})
post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
val labelId = params("value").toInt
executeBatch(repository) { issueId =>
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
registerIssueLabel(repository.owner, repository.name, issueId, labelId)
defining(params("value").toInt){ labelId =>
executeBatch(repository) { issueId =>
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
registerIssueLabel(repository.owner, repository.name, issueId, labelId)
}
}
}
})
post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
val value = assignedUserName("value")
executeBatch(repository) {
updateAssignedUserName(repository.owner, repository.name, _, value)
defining(assignedUserName("value")){ value =>
executeBatch(repository) {
updateAssignedUserName(repository.owner, repository.name, _, value)
}
}
})
post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository =>
val value = milestoneId("value")
executeBatch(repository) {
updateMilestoneId(repository.owner, repository.name, _, value)
defining(milestoneId("value")){ value =>
executeBatch(repository) {
updateMilestoneId(repository.owner, repository.name, _, value)
}
}
})
@@ -266,68 +270,89 @@ trait IssuesControllerBase extends ControllerBase {
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
(getAction: model.Issue => Option[String] =
p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
val owner = repository.owner
val name = repository.name
val userName = context.loginAccount.get.userName
getIssue(owner, name, issueId.toString) map { issue =>
val (action, recordActivity) =
getAction(issue)
.collect {
case "close" => true -> (Some("close") -> Some(recordCloseIssueActivity _))
case "reopen" => false -> (Some("reopen") -> Some(recordReopenIssueActivity _))
defining(repository.owner, repository.name){ case (owner, name) =>
val userName = context.loginAccount.get.userName
getIssue(owner, name, issueId.toString) map { issue =>
val (action, recordActivity) =
getAction(issue)
.collect {
case "close" => true -> (Some("close") ->
Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
case "reopen" => false -> (Some("reopen") ->
Some(recordReopenIssueActivity _))
}
.map { case (closed, t) =>
.map { case (closed, t) =>
updateClosed(owner, name, issueId, closed)
t
}
.getOrElse(None -> None)
.getOrElse(None -> None)
val commentId = content
val commentId = content
.map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") )
.getOrElse ( action.get.capitalize -> action.get )
match {
case (content, action) => createComment(owner, name, userName, issueId, content, action)
}
match {
case (content, action) => createComment(owner, name, userName, issueId, content, action)
}
// record activity
content foreach ( recordCommentIssueActivity(owner, name, userName, issueId, _) )
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
// record activity
content foreach {
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
(owner, name, userName, issueId, _)
}
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
commentId
// notifications
Notifier() match {
case f =>
content foreach {
f.toNotify(repository, issueId, _){
Notifier.msgComment(s"${baseUrl}/${owner}/${name}/${
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}")
}
}
action foreach {
f.toNotify(repository, issueId, _){
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
}
}
}
issue -> commentId
}
}
}
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
val owner = repository.owner
val repoName = repository.name
val filterUser = Map(filter -> params.getOrElse("userName", ""))
val page = IssueSearchCondition.page(request)
val sessionKey = s"${owner}/${repoName}/issues"
defining(repository.owner, repository.name){ case (owner, repoName) =>
val filterUser = Map(filter -> params.getOrElse("userName", ""))
val page = IssueSearchCondition.page(request)
val sessionKey = Keys.Session.Issues(owner, repoName)
// retrieve search condition
val condition = if(request.getQueryString == null){
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition]
} else IssueSearchCondition(request)
// retrieve search condition
val condition = session.putAndGet(sessionKey,
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
session.put(sessionKey, condition)
issues.html.list(
searchIssue(condition, filterUser, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page,
(getCollaborators(owner, repoName) :+ owner).sorted,
getMilestones(owner, repoName),
getLabels(owner, repoName),
countIssue(condition.copy(state = "open"), filterUser, owner -> repoName),
countIssue(condition.copy(state = "closed"), filterUser, owner -> repoName),
countIssue(condition, Map.empty, owner -> repoName),
context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), owner -> repoName)),
context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), owner -> repoName)),
countIssueGroupByLabels(owner, repoName, condition, filterUser),
condition,
filter,
repository,
hasWritePermission(owner, repoName, context.loginAccount))
issues.html.list(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page,
(getCollaborators(owner, repoName) :+ owner).sorted,
getMilestones(owner, repoName),
getLabels(owner, repoName),
countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName),
countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName),
countIssue(condition, Map.empty, false, owner -> repoName),
context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)),
context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)),
countIssueGroupByLabels(owner, repoName, condition, filterUser),
condition,
filter,
repository,
hasWritePermission(owner, repoName, context.loginAccount))
}
}
}

View File

@@ -51,7 +51,7 @@ trait LabelsControllerBase extends ControllerBase {
* Constraint for the identifier such as user name, repository name or page name.
*/
private def labelName: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
override def validate(name: String, value: String): Option[String] =
if(!value.matches("^[^,]+$")){
Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){

View File

@@ -0,0 +1,419 @@
package app
import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier, Keys}
import util.Directory._
import util.Implicits._
import util.ControlUtil._
import service._
import org.eclipse.jgit.api.Git
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.transport.RefSpec
import org.apache.commons.io.FileUtils
import scala.collection.JavaConverters._
import org.eclipse.jgit.lib.PersonIdent
import org.eclipse.jgit.api.MergeCommand.FastForwardMode
import service.IssuesService._
import service.PullRequestService._
import util.JGitUtil.DiffInfo
import scala.Some
import service.RepositoryService.RepositoryTreeNode
import util.JGitUtil.CommitInfo
class PullRequestsController extends PullRequestsControllerBase
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with ActivityService
with ReferrerAuthenticator with CollaboratorsAuthenticator
trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService with IssuesService with MilestonesService with ActivityService with PullRequestService
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
val pullRequestForm = mapping(
"title" -> trim(label("Title" , text(required, maxlength(100)))),
"content" -> trim(label("Content", optional(text()))),
"targetUserName" -> trim(text(required, maxlength(100))),
"targetBranch" -> trim(text(required, maxlength(100))),
"requestUserName" -> trim(text(required, maxlength(100))),
"requestBranch" -> trim(text(required, maxlength(100))),
"commitIdFrom" -> trim(text(required, maxlength(40))),
"commitIdTo" -> trim(text(required, maxlength(40)))
)(PullRequestForm.apply)
val mergeForm = mapping(
"message" -> trim(label("Message", text(required)))
)(MergeForm.apply)
case class PullRequestForm(
title: String,
content: Option[String],
targetUserName: String,
targetBranch: String,
requestUserName: String,
requestBranch: String,
commitIdFrom: String,
commitIdTo: String)
case class MergeForm(message: String)
get("/:owner/:repository/pulls")(referrersOnly { repository =>
searchPullRequests(None, repository)
})
get("/:owner/:repository/pulls/:userName")(referrersOnly { repository =>
searchPullRequests(Some(params("userName")), repository)
})
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
defining(repository.owner, repository.name, params("id").toInt){ case (owner, name, issueId) =>
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
using(Git.open(getRepositoryDir(owner, name))){ git =>
val (commits, diffs) =
getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
pulls.html.pullreq(
issue, pullreq,
getComments(owner, name, issueId.toInt),
(getCollaborators(owner, name) :+ owner).sorted,
getMilestonesWithIssueCount(owner, name),
commits,
diffs,
hasWritePermission(owner, name, context.loginAccount),
repository)
}
} getOrElse NotFound
}
})
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository =>
defining(repository.owner, repository.name, params("id").toInt){ case (owner, name, issueId) =>
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
pulls.html.mergeguide(
checkConflict(owner, name, pullreq.branch, owner, name, pullreq.requestBranch),
pullreq,
s"${baseUrl}${context.path}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
} getOrElse NotFound()
}
})
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
defining(repository.owner, repository.name, params("id").toInt){ case (owner, name, issueId) =>
LockUtil.lock(s"${owner}/${name}/merge"){
getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
val remote = getRepositoryDir(owner, name)
val tmpdir = new java.io.File(getTemporaryDir(owner, name), s"merge-${issueId}")
val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(pullreq.branch).call
try {
// mark issue as merged and close.
val loginAccount = context.loginAccount.get
createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
createComment(owner, name, loginAccount.userName, issueId, "Close", "close")
updateClosed(owner, name, issueId, true)
// record activity
recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
// fetch pull request to temporary working repository
val pullRequestBranchName = s"gitbucket-pullrequest-${issueId}"
git.fetch
.setRemote(getRepositoryDir(owner, name).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/pull/${issueId}/head:refs/heads/${pullRequestBranchName}")).call
// merge pull request
git.checkout.setName(pullreq.branch).call
val result = git.merge
.include(git.getRepository.resolve(pullRequestBranchName))
.setFastForward(FastForwardMode.NO_FF)
.setCommit(false)
.call
if(result.getConflicts != null){
throw new RuntimeException("This pull request can't merge automatically.")
}
// merge commit
git.getRepository.writeMergeCommitMsg(
s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestRepositoryName}\n"
+ form.message)
git.commit
.setCommitter(new PersonIdent(loginAccount.userName, loginAccount.mailAddress))
.call
// push
git.push.call
val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
commits.flatten.foreach { commit =>
if(!existsCommitId(owner, name, commit.id)){
insertCommitId(owner, name, commit.id)
}
}
// notifications
Notifier().toNotify(repository, issueId, "merge"){
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/pull/${issueId}")
}
redirect(s"/${owner}/${name}/pull/${issueId}")
} finally {
git.getRepository.close
FileUtils.deleteDirectory(tmpdir)
}
} getOrElse NotFound
}
}
})
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(originUserName), Some(originRepositoryName)) => {
getRepository(originUserName, originRepositoryName, baseUrl).map { originRepository =>
using(
Git.open(getRepositoryDir(originUserName, originRepositoryName)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
){ (oldGit, newGit) =>
val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2
val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
}
} getOrElse NotFound
}
case _ => {
using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
val defaultBranch = JGitUtil.getDefaultBranch(git, forkedRepository).get._2
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
}
}
}
})
get("/:owner/:repository/compare/*...*")(referrersOnly { repository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner)
(getRepository(originOwner, repository.name, baseUrl),
getRepository(forkedOwner, repository.name, baseUrl)) match {
case (Some(originRepository), Some(forkedRepository)) => {
using(
Git.open(getRepositoryDir(originOwner, repository.name)),
Git.open(getRepositoryDir(forkedOwner, repository.name))
){ case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
val forkedId = getForkedCommitId(oldGit, newGit,
originOwner, repository.name, originBranch,
forkedOwner, repository.name, forkedBranch)
val oldId = oldGit.getRepository.resolve(forkedId)
val newId = newGit.getRepository.resolve(forkedBranch)
val (commits, diffs) = getRequestCompareInfo(
originOwner, repository.name, oldId.getName,
forkedOwner, repository.name, newId.getName)
pulls.html.compare(
commits,
diffs,
repository.repository.originUserName.map { userName =>
userName :: getForkedRepositories(userName, repository.name)
} getOrElse List(repository.owner),
originBranch,
forkedBranch,
oldId.getName,
newId.getName,
repository,
originRepository,
forkedRepository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
}
}
case _ => NotFound
}
})
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { repository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner)
(getRepository(originOwner, repository.name, baseUrl),
getRepository(forkedOwner, repository.name, baseUrl)) match {
case (Some(originRepository), Some(forkedRepository)) => {
using(
Git.open(getRepositoryDir(originOwner, repository.name)),
Git.open(getRepositoryDir(forkedOwner, repository.name))
){ case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
pulls.html.mergecheck(
checkConflict(originOwner, repository.name, originBranch, forkedOwner, repository.name, forkedBranch))
}
}
case _ => NotFound()
}
})
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
val loginUserName = context.loginAccount.get.userName
val issueId = createIssue(
owner = repository.owner,
repository = repository.name,
loginUser = loginUserName,
title = form.title,
content = form.content,
assignedUserName = None,
milestoneId = None,
isPullRequest = true)
createPullRequest(
originUserName = repository.owner,
originRepositoryName = repository.name,
issueId = issueId,
originBranch = form.targetBranch,
requestUserName = form.requestUserName,
requestRepositoryName = repository.name,
requestBranch = form.requestBranch,
commitIdFrom = form.commitIdFrom,
commitIdTo = form.commitIdTo)
// fetch requested branch
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.fetch
.setRemote(getRepositoryDir(form.requestUserName, repository.name).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head"))
.call
}
// record activity
recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title)
// notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgPullRequest(s"${baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
}
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
})
/**
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
*/
private def checkConflict(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
// TODO Are there more quick way?
LockUtil.lock(s"${userName}/${repositoryName}/merge-check"){
val remote = getRepositoryDir(userName, repositoryName)
val tmpdir = new java.io.File(getTemporaryDir(userName, repositoryName), "merge-check")
if(tmpdir.exists()){
FileUtils.deleteDirectory(tmpdir)
}
val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(branch).call
try {
git.checkout.setName(branch).call
git.fetch
.setRemote(getRepositoryDir(requestUserName, requestRepositoryName).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/heads/${requestBranch}")).call
val result = git.merge
.include(git.getRepository.resolve("FETCH_HEAD"))
.setCommit(false).call
result.getConflicts != null
} finally {
git.getRepository.close
FileUtils.deleteDirectory(tmpdir)
}
}
}
/**
* Parses branch identifier and extracts owner and branch name as tuple.
*
* - "owner:branch" to ("owner", "branch")
* - "branch" to ("defaultOwner", "branch")
*/
private def parseCompareIdentifie(value: String, defaultOwner: String): (String, String) =
if(value.contains(':')){
val array = value.split(":")
(array(0), array(1))
} else {
(defaultOwner, value)
}
/**
* Extracts all repository names from [[service.RepositoryService.RepositoryTreeNode]] as flat list.
*/
private def getRepositoryNames(node: RepositoryTreeNode): List[String] =
node.owner :: node.children.map { child => getRepositoryNames(child) }.flatten
/**
* Returns the identifier of the root commit (or latest merge commit) of the specified branch.
*/
private def getForkedCommitId(oldGit: Git, newGit: Git, userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): String =
JGitUtil.getCommitLogs(newGit, requestBranch, true){ commit =>
existsCommitId(userName, repositoryName, commit.getName) &&
JGitUtil.getBranchesOfCommit(oldGit, commit.getName).contains(branch)
}.head.id
private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = {
using(
Git.open(getRepositoryDir(userName, repositoryName)),
Git.open(getRepositoryDir(requestUserName, requestRepositoryName))
){ (oldGit, newGit) =>
val oldId = oldGit.getRepository.resolve(branch)
val newId = newGit.getRepository.resolve(requestCommitId)
val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
new CommitInfo(revCommit)
}.toList.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
}
val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true)
(commits, diffs)
}
}
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
defining(repository.owner, repository.name){ case (owner, repoName) =>
val filterUser = userName.map { x => Map("created_by" -> x) } getOrElse Map("all" -> "")
val page = IssueSearchCondition.page(request)
val sessionKey = Keys.Session.Pulls(owner, repoName)
// retrieve search condition
val condition = session.putAndGet(sessionKey,
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
pulls.html.list(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
getPullRequestCountGroupByUser(condition.state == "closed", owner, Some(repoName)),
userName,
page,
countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName),
countIssue(condition.copy(state = "closed"), filterUser, true, owner -> repoName),
countIssue(condition, Map.empty, true, owner -> repoName),
condition,
repository,
hasWritePermission(owner, repoName, context.loginAccount))
}
}

View File

@@ -6,13 +6,20 @@ import util.{UsersAuthenticator, OwnerAuthenticator}
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.FlashMapSupport
import service.WebHookService.WebHookPayload
import util.JGitUtil.CommitInfo
import util.ControlUtil._
import org.eclipse.jgit.api.Git
class RepositorySettingsController extends RepositorySettingsControllerBase
with RepositoryService with AccountService with OwnerAuthenticator with UsersAuthenticator
with RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator
trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSupport {
self: RepositoryService with AccountService with OwnerAuthenticator with UsersAuthenticator =>
self: RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator =>
// for repository options
case class OptionsForm(description: Option[String], defaultBranch: String, isPrivate: Boolean)
val optionsForm = mapping(
@@ -20,13 +27,21 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
"isPrivate" -> trim(label("Repository Type", boolean()))
)(OptionsForm.apply)
// for collaborator addition
case class CollaboratorForm(userName: String)
val collaboratorForm = mapping(
"userName" -> trim(label("Username", text(required, collaborator)))
)(CollaboratorForm.apply)
// for web hook url addition
case class WebHookForm(url: String)
val webHookForm = mapping(
"url" -> trim(label("url", text(required, webHook)))
)(WebHookForm.apply)
/**
* Redirect to the Options page.
*/
@@ -45,7 +60,15 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
* Save the repository options.
*/
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
saveRepositoryOptions(repository.owner, repository.name, form.description, form.defaultBranch, form.isPrivate)
saveRepositoryOptions(
repository.owner,
repository.name,
form.description,
form.defaultBranch,
repository.repository.parentUserName.map { _ =>
repository.repository.isPrivate
} getOrElse form.isPrivate
)
flash += "info" -> "Repository settings has been updated."
redirect(s"/${repository.owner}/${repository.name}/settings/options")
})
@@ -80,6 +103,53 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
})
/**
* Display the web hook page.
*/
get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
settings.html.hooks(getWebHookURLs(repository.owner, repository.name), repository, flash.get("info"))
})
/**
* Add the web hook URL.
*/
post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) =>
addWebHookURL(repository.owner, repository.name, form.url)
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
})
/**
* Delete the web hook URL.
*/
get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository =>
deleteWebHookURL(repository.owner, repository.name, params("url"))
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
})
/**
* Send the test request to registered web hook URLs.
*/
get("/:owner/:repository/settings/hooks/test")(ownerOnly { repository =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
import scala.collection.JavaConverters._
val commits = git.log
.add(git.getRepository.resolve(repository.repository.defaultBranch))
.setMaxCount(3)
.call.iterator.asScala.map(new CommitInfo(_))
callWebHook(repository.owner, repository.name,
WebHookPayload(
git,
"refs/heads/" + repository.repository.defaultBranch,
repository,
commits.toList,
getAccountByUserName(repository.owner).get))
flash += "info" -> "Test payload deployed!"
}
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
})
/**
* Display the delete repository page.
*/
@@ -100,19 +170,25 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
redirect(s"/${repository.owner}")
})
/**
* Provides duplication check for web hook url.
*/
private def webHook: Constraint = new Constraint(){
override def validate(name: String, value: String): Option[String] =
getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.")
}
/**
* Provides Constraint to validate the collaborator name.
*/
private def collaborator: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] = {
val paths = request.getRequestURI.split("/")
override def validate(name: String, value: String): Option[String] =
getAccountByUserName(value) match {
case None => Some("User does not exist.")
case Some(x) if(x.userName == paths(1) || getCollaborators(paths(1), paths(2)).contains(x.userName))
case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName))
=> Some("User can access this repository already.")
case _ => None
}
}
}
}

View File

@@ -2,7 +2,8 @@ package app
import util.Directory._
import util.Implicits._
import _root_.util.{ReferrerAuthenticator, JGitUtil, FileUtil}
import util.ControlUtil._
import _root_.util.{ReferrerAuthenticator, JGitUtil, FileUtil, StringUtil}
import service._
import org.scalatra._
import java.io.File
@@ -56,7 +57,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val (branchName, path) = splitPath(repository, multiParams("splat").head)
val page = params.getOrElse("page", "1").toInt
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
case Right((logs, hasNext)) =>
repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
@@ -75,7 +76,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val (id, path) = splitPath(repository, multiParams("splat").head)
val raw = params.get("raw").getOrElse("false").toBoolean
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
@scala.annotation.tailrec
@@ -84,19 +85,25 @@ trait RepositoryViewerControllerBase extends ControllerBase {
case true => getPathObjectId(path, walk)
}
val treeWalk = new TreeWalk(git.getRepository)
val objectId = try {
val objectId = using(new TreeWalk(git.getRepository)){ treeWalk =>
treeWalk.addTree(revCommit.getTree)
treeWalk.setRecursive(true)
getPathObjectId(path, treeWalk)
} finally {
treeWalk.release
}
if(raw){
// Download
contentType = "application/octet-stream"
JGitUtil.getContent(git, objectId, false).get
val mimeType = FileUtil.getMimeType(path)
val bytes = JGitUtil.getContent(git, objectId, false).get
contentType = if(mimeType == "application/octet-stream" && FileUtil.isText(bytes)){
"text/plain"
} else {
mimeType
}
//response.setHeader("Content-Disposition", s"inline; filename=${FileUtil.getFileName(path)}")
bytes
} else {
// Viewer
val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize)
@@ -106,7 +113,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val content = if(viewer == "other"){
if(bytes.isDefined && FileUtil.isText(bytes.get)){
// text
JGitUtil.ContentInfo("text", bytes.map(new String(_, "UTF-8")))
JGitUtil.ContentInfo("text", bytes.map(StringUtil.convertFromByteArray))
} else {
// binary
JGitUtil.ContentInfo("binary", None)
@@ -127,22 +134,39 @@ trait RepositoryViewerControllerBase extends ControllerBase {
get("/:owner/:repository/commit/:id")(referrersOnly { repository =>
val id = params("id")
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
JGitUtil.getBranchesOfCommit(git, revCommit.getName), JGitUtil.getTagsOfCommit(git, revCommit.getName),
repository, JGitUtil.getDiffs(git, id))
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))){ revCommit =>
JGitUtil.getDiffs(git, id) match { case (diffs, oldCommitId) =>
repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
JGitUtil.getTagsOfCommit(git, revCommit.getName),
repository, diffs, oldCommitId)
}
}
}
})
/**
* Displays branches.
*/
get("/:owner/:repository/branches")(referrersOnly { repository =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
// retrieve latest update date of each branch
val branchInfo = repository.branchList.map { branchName =>
val revCommit = git.log.add(git.getRepository.resolve(branchName)).setMaxCount(1).call.iterator.next
(branchName, revCommit.getCommitterIdent.getWhen)
}
repo.html.branches(branchInfo, repository)
}
})
/**
* Displays tags.
*/
get("/:owner/:repository/tags")(referrersOnly {
repo.html.tags(_)
})
/**
* Download repository contents as an archive.
*/
@@ -159,7 +183,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
// clone the repository
val cloneDir = new File(workDir, revision)
JGitUtil.withGit(Git.cloneRepository
using(Git.cloneRepository
.setURI(getRepositoryDir(repository.owner, repository.name).toURI.toString)
.setDirectory(cloneDir)
.call){ git =>
@@ -182,12 +206,24 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
})
get("/:owner/:repository/network/members")(referrersOnly { repository =>
repo.html.forked(
getRepository(
repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name),
baseUrl),
getForkedRepositories(
repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name)),
repository)
})
private def splitPath(repository: service.RepositoryService.RepositoryInfo, path: String): (String, String) = {
val id = repository.branchList.collectFirst {
case branch if(path == branch || path.startsWith(branch + "/")) => branch
} orElse repository.tags.collectFirst {
case tag if(path == tag.name || path.startsWith(tag.name + "/")) => tag.name
} orElse Some(path) get
} orElse Some(path.split("/")(0)) get
(id, path.substring(id.length).replaceFirst("^/", ""))
}
@@ -204,23 +240,23 @@ trait RepositoryViewerControllerBase extends ControllerBase {
if(repository.commitCount == 0){
repo.html.guide(repository)
} else {
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head)
// get specified commit
revisions.map { rev => (git.getRepository.resolve(rev), rev)}.find(_._1 != null).map { case (objectId, revision) =>
val revCommit = JGitUtil.getRevCommitFromId(git, objectId)
JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
defining(JGitUtil.getRevCommitFromId(git, objectId)){ revCommit =>
// get files
val files = JGitUtil.getFileList(git, revision, path)
// process README.md
val readme = files.find(_.name == "README.md").map { file =>
new String(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get, "UTF-8")
}
val files = JGitUtil.getFileList(git, revision, path)
// process README.md
val readme = files.find(_.name == "README.md").map { file =>
StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
}
repo.html.files(revision, repository,
if(path == ".") Nil else path.split("/").toList, // current path
new JGitUtil.CommitInfo(revCommit), // latest commit
files, readme)
repo.html.files(revision, repository,
if(path == ".") Nil else path.split("/").toList, // current path
new JGitUtil.CommitInfo(revCommit), // latest commit
files, readme)
}
} getOrElse NotFound
}
}

View File

@@ -1,6 +1,7 @@
package app
import util._
import ControlUtil._
import service._
import jp.sf.amateras.scalatra.forms._
@@ -26,25 +27,25 @@ trait SearchControllerBase extends ControllerBase { self: RepositoryService
}
get("/:owner/:repository/search")(referrersOnly { repository =>
val query = params("q").trim
val target = params.getOrElse("type", "code")
val page = try {
val i = params.getOrElse("page", "1").toInt
if(i <= 0) 1 else i
} catch {
case e: NumberFormatException => 1
}
defining(params("q").trim, params.getOrElse("type", "code")){ case (query, target) =>
val page = try {
val i = params.getOrElse("page", "1").toInt
if(i <= 0) 1 else i
} catch {
case e: NumberFormatException => 1
}
target.toLowerCase match {
case "issue" => search.html.issues(
searchIssues(repository.owner, repository.name, query),
countFiles(repository.owner, repository.name, query),
query, page, repository)
target.toLowerCase match {
case "issue" => search.html.issues(
searchIssues(repository.owner, repository.name, query),
countFiles(repository.owner, repository.name, query),
query, page, repository)
case _ => search.html.code(
searchFiles(repository.owner, repository.name, query),
countIssues(repository.owner, repository.name, query),
query, page, repository)
case _ => search.html.code(
searchFiles(repository.owner, repository.name, query),
countIssues(repository.owner, repository.name, query),
query, page, repository)
}
}
})

View File

@@ -1,8 +1,9 @@
package app
import service._
import util.StringUtil._
import jp.sf.amateras.scalatra.forms._
import util.Implicits._
import util.Keys
class SignInController extends SignInControllerBase with SystemSettingsService with AccountService
@@ -16,27 +17,18 @@ trait SignInControllerBase extends ControllerBase { self: SystemSettingsService
)(SignInForm.apply)
get("/signin"){
val queryString = request.getQueryString
if(queryString != null && queryString.startsWith("/")){
session.setAttribute("REDIRECT", queryString)
val redirect = params.get("redirect")
if(redirect.isDefined && redirect.get.startsWith("/")){
session.setAttribute(Keys.Session.Redirect, redirect.get)
}
html.signin(loadSystemSettings())
}
post("/signin", form){ form =>
getAccountByUserName(form.userName).collect {
case account if(!account.isGroupAccount && account.password == sha1(form.password)) => {
session.setAttribute("LOGIN_ACCOUNT", account)
updateLastLoginDate(account.userName)
session.get("REDIRECT").map { redirectUrl =>
session.removeAttribute("REDIRECT")
redirect(redirectUrl.asInstanceOf[String])
}.getOrElse {
redirect("/")
}
}
} getOrElse redirect("/signin")
authenticate(loadSystemSettings(), form.userName, form.password) match {
case Some(account) => signin(account)
case None => redirect("/signin")
}
}
get("/signout"){
@@ -44,4 +36,22 @@ trait SignInControllerBase extends ControllerBase { self: SystemSettingsService
redirect("/")
}
/**
* Set account information into HttpSession and redirect.
*/
private def signin(account: model.Account) = {
session.setAttribute(Keys.Session.LoginAccount, account)
updateLastLoginDate(account.userName)
session.getAndRemove[String](Keys.Session.Redirect).map { redirectUrl =>
if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){
redirect("/")
} else {
redirect(redirectUrl)
}
}.getOrElse {
redirect("/")
}
}
}

View File

@@ -12,11 +12,28 @@ class SystemSettingsController extends SystemSettingsControllerBase
trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
self: SystemSettingsService with AccountService with AdminAuthenticator =>
private case class SystemSettingsForm(allowAccountRegistration: Boolean)
private val form = mapping(
"allowAccountRegistration" -> trim(label("Account registration", boolean()))
)(SystemSettingsForm.apply)
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
"gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())),
"smtp" -> optionalIfNotChecked("notification", mapping(
"host" -> trim(label("SMTP Host", text(required))),
"port" -> trim(label("SMTP Port", optional(number()))),
"user" -> trim(label("SMTP User", optional(text()))),
"password" -> trim(label("SMTP Password", optional(text()))),
"ssl" -> trim(label("Enable SSL", optional(boolean())))
)(Smtp.apply)),
"ldapAuthentication" -> trim(label("LDAP", boolean())),
"ldap" -> optionalIfNotChecked("ldapAuthentication", mapping(
"host" -> trim(label("LDAP host", text(required))),
"port" -> trim(label("LDAP port", optional(number()))),
"bindDN" -> trim(label("Bind DN", optional(text()))),
"bindPassword" -> trim(label("Bind Password", optional(text()))),
"baseDN" -> trim(label("Base DN", text(required))),
"userNameAttribute" -> trim(label("User name attribute", text(required))),
"mailAttribute" -> trim(label("Mail address attribute", text(required)))
)(Ldap.apply))
)(SystemSettings.apply)
get("/admin/system")(adminOnly {
@@ -24,7 +41,7 @@ trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
})
post("/admin/system", form)(adminOnly { form =>
saveSystemSettings(SystemSettings(form.allowAccountRegistration))
saveSystemSettings(form)
flash += "info" -> "System settings has been updated."
redirect("/admin/system")
})

View File

@@ -3,6 +3,7 @@ package app
import service._
import util.AdminAuthenticator
import util.StringUtil._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._
class UserManagementController extends UserManagementControllerBase
@@ -107,29 +108,29 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
})
get("/admin/users/:groupName/_editgroup")(adminOnly {
val groupName = params("groupName")
admin.users.html.group(getAccountByUserName(groupName), getGroupMembers(groupName))
defining(params("groupName")){ groupName =>
admin.users.html.group(getAccountByUserName(groupName), getGroupMembers(groupName))
}
})
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
val groupName = params("groupName")
getAccountByUserName(groupName).map { account =>
updateGroup(groupName, form.url)
defining(params("groupName"), form.memberNames.map(_.split(",").toList).getOrElse(Nil)){ case (groupName, memberNames) =>
getAccountByUserName(groupName).map { account =>
updateGroup(groupName, form.url)
updateGroupMembers(form.groupName, memberNames)
val memberNames = form.memberNames.map(_.split(",").toList).getOrElse(Nil)
updateGroupMembers(form.groupName, memberNames)
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
removeCollaborators(form.groupName, repositoryName)
memberNames.foreach { userName =>
addCollaborator(form.groupName, repositoryName, userName)
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
removeCollaborators(form.groupName, repositoryName)
memberNames.foreach { userName =>
addCollaborator(form.groupName, repositoryName, userName)
}
}
}
updateImage(form.groupName, form.fileId, form.clearImage)
redirect("/admin/users")
updateImage(form.groupName, form.fileId, form.clearImage)
redirect("/admin/users")
} getOrElse NotFound
} getOrElse NotFound
}
})
post("/admin/users/_usercheck")(adminOnly {

View File

@@ -3,7 +3,9 @@ package app
import service._
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, JGitUtil, StringUtil}
import util.Directory._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.api.Git
class WikiController extends WikiControllerBase
with WikiService with RepositoryService with AccountService with ActivityService
@@ -40,13 +42,13 @@ trait WikiControllerBase extends ControllerBase {
getWikiPage(repository.owner, repository.name, pageName).map { page =>
wiki.html.page(pageName, page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${pageName}/_edit") // TODO URLEncode
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
})
get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository =>
val pageName = StringUtil.urlDecode(params("page"))
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match {
case Right((logs, hasNext)) => wiki.html.history(Some(pageName), logs, repository)
case Left(_) => NotFound
@@ -58,16 +60,16 @@ trait WikiControllerBase extends ControllerBase {
val pageName = StringUtil.urlDecode(params("page"))
val commitId = params("commitId").split("\\.\\.\\.")
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
wiki.html.compare(Some(pageName), getWikiDiffs(git, commitId(0), commitId(1)), repository)
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
wiki.html.compare(Some(pageName), JGitUtil.getDiffs(git, commitId(0), commitId(1), true), repository)
}
})
get("/:owner/:repository/wiki/_compare/:commitId")(referrersOnly { repository =>
val commitId = params("commitId").split("\\.\\.\\.")
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
wiki.html.compare(None, getWikiDiffs(git, commitId(0), commitId(1)), repository)
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
wiki.html.compare(None, JGitUtil.getDiffs(git, commitId(0), commitId(1), true), repository)
}
})
@@ -80,10 +82,10 @@ trait WikiControllerBase extends ControllerBase {
val loginAccount = context.loginAccount.get
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
form.content, loginAccount, form.message.getOrElse(""))
updateLastActivityDate(repository.owner, repository.name)
recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
form.content, loginAccount, form.message.getOrElse("")).map { commitId =>
updateLastActivityDate(repository.owner, repository.name)
recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
}
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
})
@@ -105,9 +107,10 @@ trait WikiControllerBase extends ControllerBase {
})
get("/:owner/:repository/wiki/:page/_delete")(collaboratorsOnly { repository =>
val pageName = StringUtil.urlDecode(params("page"))
val pageName = StringUtil.urlDecode(params("page"))
val account = context.loginAccount.get
deleteWikiPage(repository.owner, repository.name, pageName, context.loginAccount.get.userName, s"Delete ${pageName}")
deleteWikiPage(repository.owner, repository.name, pageName, account.userName, account.mailAddress, s"Delete ${pageName}")
updateLastActivityDate(repository.owner, repository.name)
redirect(s"/${repository.owner}/${repository.name}/wiki")
@@ -119,7 +122,7 @@ trait WikiControllerBase extends ControllerBase {
})
get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, "master") match {
case Right((logs, hasNext)) => wiki.html.history(None, logs, repository)
case Left(_) => NotFound
@@ -129,18 +132,18 @@ trait WikiControllerBase extends ControllerBase {
get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository =>
getFileContent(repository.owner, repository.name, multiParams("splat").head).map { content =>
contentType = "application/octet-stream"
content
contentType = "application/octet-stream"
content
} getOrElse NotFound
})
private def unique: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
getWikiPageList(params("owner"), params("repository")).find(_ == value).map(_ => "Page already exists.")
}
private def pagename: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
override def validate(name: String, value: String): Option[String] =
if(value.exists("\\/:*?\"<>|".contains(_))){
Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){

View File

@@ -20,7 +20,8 @@ object Issues extends Table[Issue]("ISSUE") with IssueTemplate with MilestoneTem
def closed = column[Boolean]("CLOSED")
def registeredDate = column[java.util.Date]("REGISTERED_DATE")
def updatedDate = column[java.util.Date]("UPDATED_DATE")
def * = userName ~ repositoryName ~ issueId ~ openedUserName ~ milestoneId.? ~ assignedUserName.? ~ title ~ content.? ~ closed ~ registeredDate ~ updatedDate <> (Issue, Issue.unapply _)
def pullRequest = column[Boolean]("PULL_REQUEST")
def * = userName ~ repositoryName ~ issueId ~ openedUserName ~ milestoneId.? ~ assignedUserName.? ~ title ~ content.? ~ closed ~ registeredDate ~ updatedDate ~ pullRequest <> (Issue, Issue.unapply _)
def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId)
}
@@ -36,4 +37,5 @@ case class Issue(
content: Option[String],
closed: Boolean,
registeredDate: java.util.Date,
updatedDate: java.util.Date)
updatedDate: java.util.Date,
isPullRequest: Boolean)

View File

@@ -0,0 +1,28 @@
package model
import scala.slick.driver.H2Driver.simple._
object PullRequests extends Table[PullRequest]("PULL_REQUEST") with IssueTemplate {
def branch = column[String]("BRANCH")
def requestUserName = column[String]("REQUEST_USER_NAME")
def requestRepositoryName = column[String]("REQUEST_REPOSITORY_NAME")
def requestBranch = column[String]("REQUEST_BRANCH")
def commitIdFrom = column[String]("COMMIT_ID_FROM")
def commitIdTo = column[String]("COMMIT_ID_TO")
def * = userName ~ repositoryName ~ issueId ~ branch ~ requestUserName ~ requestRepositoryName ~ requestBranch ~ commitIdFrom ~ commitIdTo <> (PullRequest, PullRequest.unapply _)
def byPrimaryKey(userName: String, repositoryName: String, issueId: Int) = byIssue(userName, repositoryName, issueId)
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = byIssue(userName, repositoryName, issueId)
}
case class PullRequest(
userName: String,
repositoryName: String,
issueId: Int,
branch: String,
requestUserName: String,
requestRepositoryName: String,
requestBranch: String,
commitIdFrom: String,
commitIdTo: String
)

View File

@@ -9,7 +9,11 @@ object Repositories extends Table[Repository]("REPOSITORY") with BasicTemplate {
def registeredDate = column[java.util.Date]("REGISTERED_DATE")
def updatedDate = column[java.util.Date]("UPDATED_DATE")
def lastActivityDate = column[java.util.Date]("LAST_ACTIVITY_DATE")
def * = userName ~ repositoryName ~ isPrivate ~ description.? ~ defaultBranch ~ registeredDate ~ updatedDate ~ lastActivityDate <> (Repository, Repository.unapply _)
def originUserName = column[String]("ORIGIN_USER_NAME")
def originRepositoryName = column[String]("ORIGIN_REPOSITORY_NAME")
def parentUserName = column[String]("PARENT_USER_NAME")
def parentRepositoryName = column[String]("PARENT_REPOSITORY_NAME")
def * = userName ~ repositoryName ~ isPrivate ~ description.? ~ defaultBranch ~ registeredDate ~ updatedDate ~ lastActivityDate ~ originUserName.? ~ originRepositoryName.? ~ parentUserName.? ~ parentRepositoryName.? <> (Repository, Repository.unapply _)
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
}
@@ -22,5 +26,9 @@ case class Repository(
defaultBranch: String,
registeredDate: java.util.Date,
updatedDate: java.util.Date,
lastActivityDate: java.util.Date
lastActivityDate: java.util.Date,
originUserName: Option[String],
originRepositoryName: Option[String],
parentUserName: Option[String],
parentRepositoryName: Option[String]
)

View File

@@ -0,0 +1,16 @@
package model
import scala.slick.driver.H2Driver.simple._
object WebHooks extends Table[WebHook]("WEB_HOOK") with BasicTemplate {
def url = column[String]("URL")
def * = userName ~ repositoryName ~ url <> (WebHook, WebHook.unapply _)
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url is url.bind)
}
case class WebHook(
userName: String,
repositoryName: String,
url: String
)

View File

@@ -3,9 +3,48 @@ package service
import model._
import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession
import service.SystemSettingsService.SystemSettings
import util.StringUtil._
import model.GroupMember
import scala.Some
import model.Account
import util.LDAPUtil
trait AccountService {
def authenticate(settings: SystemSettings, userName: String, password: String): Option[Account] =
if(settings.ldapAuthentication){
ldapAuthentication(settings, userName, password)
} else {
defaultAuthentication(userName, password)
}
/**
* Authenticate by internal database.
*/
private def defaultAuthentication(userName: String, password: String) = {
getAccountByUserName(userName).collect {
case account if(!account.isGroupAccount && account.password == sha1(password)) => Some(account)
} getOrElse None
}
/**
* Authenticate by LDAP.
*/
private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) = {
LDAPUtil.authenticate(settings.ldap.get, userName, password) match {
case Right(mailAddress) => {
// Create or update account by LDAP information
getAccountByUserName(userName) match {
case Some(x) => updateAccount(x.copy(mailAddress = mailAddress))
case None => createAccount(userName, "", mailAddress, false, None)
}
getAccountByUserName(userName)
}
case Left(errorMessage) => defaultAuthentication(userName, password)
}
}
def getAccountByUserName(userName: String): Option[Account] =
Query(Accounts) filter(_.userName is userName.bind) firstOption

View File

@@ -6,23 +6,23 @@ import Database.threadLocalSession
trait ActivityService {
def getActivitiesByUser(activityUserName: String, isPublic: Boolean): List[Activity] = {
val q = Query(Activities)
def getActivitiesByUser(activityUserName: String, isPublic: Boolean): List[Activity] =
Activities
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
.filter { case (t1, t2) =>
if(isPublic){
(t1.activityUserName is activityUserName.bind) && (t2.isPrivate is false.bind)
} else {
(t1.activityUserName is activityUserName.bind)
}
}
.sortBy { case (t1, t2) => t1.activityId desc }
.map { case (t1, t2) => t1 }
.take(30)
.list
(if(isPublic){
q filter { case (t1, t2) => (t1.activityUserName is activityUserName.bind) && (t2.isPrivate is false.bind) }
} else {
q filter { case (t1, t2) => t1.activityUserName is activityUserName.bind }
})
.sortBy { case (t1, t2) => t1.activityId desc }
.map { case (t1, t2) => t1 }
.take(30)
.list
}
def getRecentActivities(): List[Activity] =
Query(Activities)
Activities
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
.filter { case (t1, t2) => t2.isPrivate is false.bind }
.sortBy { case (t1, t2) => t1.activityId desc }
@@ -52,6 +52,13 @@ trait ActivityService {
Some(title),
currentDate)
def recordClosePullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"close_issue",
s"[user:${activityUserName}] closed pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(title),
currentDate)
def recordReopenIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"reopen_issue",
@@ -65,7 +72,14 @@ trait ActivityService {
s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(cut(comment, 200)),
currentDate)
def recordCommentPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String): Unit =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"comment_issue",
s"[user:${activityUserName}] commented on pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(cut(comment, 200)),
currentDate)
def recordCreateWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String) =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"create_wiki",
@@ -73,11 +87,11 @@ trait ActivityService {
Some(pageName),
currentDate)
def recordEditWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String) =
def recordEditWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String, commitId: String) =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"edit_wiki",
s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki",
Some(pageName),
Some(pageName + ":" + commitId),
currentDate)
def recordPushActivity(userName: String, repositoryName: String, activityUserName: String,
@@ -98,11 +112,32 @@ trait ActivityService {
def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"create_tag",
"create_branch",
s"[user:${activityUserName}] created branch [tag:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
None,
currentDate)
def recordForkActivity(userName: String, repositoryName: String, activityUserName: String) =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"fork",
s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${activityUserName}/${repositoryName}]",
None,
currentDate)
def recordPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"open_pullreq",
s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(title),
currentDate)
def recordMergeActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, message: String): Unit =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"merge_pullreq",
s"[user:${activityUserName}] merged pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(message),
currentDate)
def insertCommitId(userName: String, repositoryName: String, commitId: String) = {
CommitLog insert (userName, repositoryName, commitId)
}

View File

@@ -44,14 +44,16 @@ trait IssuesService {
*
* @param condition the search condition
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request.
* @param repos Tuple of the repository owner and the repository name
* @return the count of the search result
*/
def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], repos: (String, String)*): Int = {
def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
repos: (String, String)*): Int = {
// TODO It must be _.length instead of map (_.issueId) list).length.
// But it does not work on Slick 1.0.1 (worked on Slick 1.0.0).
// https://github.com/slick/slick/issues/170
(searchIssueQuery(repos, condition, filterUser) map (_.issueId) list).length
(searchIssueQuery(repos, condition, filterUser, onlyPullRequest) map (_.issueId) list).length
}
/**
* Returns the Map which contains issue count for each labels.
@@ -65,7 +67,7 @@ trait IssuesService {
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
filterUser: Map[String, String]): Map[String, Int] = {
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser)
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false)
.innerJoin(IssueLabels).on { (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
}
@@ -86,19 +88,21 @@ trait IssuesService {
*
* @param condition the search condition
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @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, filterUser: Map[String, String],
repos: (String, String)*): List[(String, String, Int)] = {
searchIssueQuery(repos, condition.copy(repo = None), filterUser)
def countIssueGroupByRepository(
condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
repos: (String, String)*): List[(String, String, Int)] = {
searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest)
.groupBy { t =>
t.userName ~ t.repositoryName
}
.map { case (repo, t) =>
repo ~ t.length
}
.filter (_._3 > 0.bind)
.sortBy(_._3 desc)
.list
}
@@ -106,17 +110,18 @@ trait IssuesService {
* Returns the search result against issues.
*
* @param condition the search condition
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @param filterUser the filter user name (key is "all", "assigned", "created_by" or "not_created_by", value is the user name)
* @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
* @param offset the offset for pagination
* @param limit the limit for pagination
* @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, filterUser: Map[String, String],
def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
offset: Int, limit: Int, repos: (String, String)*): List[(Issue, List[Label], Int)] = {
// get issues and comment count and labels
searchIssueQuery(repos, condition, filterUser)
searchIssueQuery(repos, condition, filterUser, onlyPullRequest)
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
.leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
.leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
@@ -156,17 +161,21 @@ trait IssuesService {
/**
* Assembles query for conditional issue searching.
*/
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, filterUser: Map[String, String]) =
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition,
filterUser: Map[String, String], onlyPullRequest: Boolean) =
Query(Issues) filter { t1 =>
(condition.repo
condition.repo
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
.getOrElse (repos)
.map { case (owner, repository) => t1.byRepository(owner, repository) } reduceLeft ( _ || _ ) ) &&
.map { case (owner, repository) => t1.byRepository(owner, repository) }
.foldLeft[Column[Boolean]](false) ( _ || _ ) &&
(t1.closed is (condition.state == "closed").bind) &&
(t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
(t1.milestoneId isNull, condition.milestoneId == Some(None)) &&
(t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) &&
(t1.openedUserName is filterUser("created_by").bind, filterUser.get("created_by").isDefined) &&
(t1.openedUserName isNot filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) &&
(t1.pullRequest is true.bind, onlyPullRequest) &&
(IssueLabels filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
(t2.labelId in
@@ -178,7 +187,7 @@ trait IssuesService {
}
def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
assignedUserName: Option[String], milestoneId: Option[Int]) =
assignedUserName: Option[String], milestoneId: Option[Int], isPullRequest: Boolean = false) =
// next id number
sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int]
.firstOption.filter { id =>
@@ -193,7 +202,8 @@ trait IssuesService {
content,
false,
currentDate,
currentDate)
currentDate,
isPullRequest)
// increment issue id
IssueId

View File

@@ -0,0 +1,57 @@
package service
import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession
import model._
import util.ControlUtil._
trait PullRequestService { self: IssuesService =>
import PullRequestService._
def getPullRequest(owner: String, repository: String, issueId: Int): Option[(Issue, PullRequest)] =
defining(getIssue(owner, repository, issueId.toString)){ issue =>
if(issue.isDefined){
Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).firstOption match {
case Some(pullreq) => Some((issue.get, pullreq))
case None => None
}
} else None
}
def getPullRequestCountGroupByUser(closed: Boolean, owner: String, repository: Option[String]): List[PullRequestCount] =
Query(PullRequests)
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
.filter { case (t1, t2) =>
(t2.closed is closed.bind) &&
(t1.userName is owner.bind) &&
(t1.repositoryName is repository.get.bind, repository.isDefined)
}
.groupBy { case (t1, t2) => 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,
commitIdFrom: String, commitIdTo: String): Unit =
PullRequests insert (PullRequest(
originUserName,
originRepositoryName,
issueId,
originBranch,
requestUserName,
requestRepositoryName,
requestBranch,
commitIdFrom,
commitIdTo))
}
object PullRequestService {
val PullRequestLimit = 25
case class PullRequestCount(userName: String, count: Int)
}

View File

@@ -1,8 +1,8 @@
package service
import model.Issue
import util.{FileUtil, StringUtil, JGitUtil}
import util.Directory._
import util.ControlUtil._
import model.Issue
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.TreeWalk
@@ -10,7 +10,8 @@ import scala.collection.mutable.ListBuffer
import org.eclipse.jgit.lib.FileMode
import org.eclipse.jgit.api.Git
trait RepositorySearchService { self: IssuesService =>
trait
RepositorySearchService { self: IssuesService =>
import RepositorySearchService._
def countIssues(owner: String, repository: String, query: String): Int =
@@ -28,12 +29,12 @@ trait RepositorySearchService { self: IssuesService =>
}
def countFiles(owner: String, repository: String, query: String): Int =
JGitUtil.withGit(getRepositoryDir(owner, repository)){ git =>
using(Git.open(getRepositoryDir(owner, repository))){ git =>
if(JGitUtil.isEmpty(git)) 0 else searchRepositoryFiles(git, query).length
}
def searchFiles(owner: String, repository: String, query: String): List[FileSearchResult] =
JGitUtil.withGit(getRepositoryDir(owner, repository)){ git =>
using(Git.open(getRepositoryDir(owner, repository))){ git =>
if(JGitUtil.isEmpty(git)){
Nil
} else {
@@ -65,7 +66,7 @@ trait RepositorySearchService { self: IssuesService =>
if(treeWalk.getFileMode(0) != FileMode.TREE){
JGitUtil.getContent(git, treeWalk.getObjectId(0), false).foreach { bytes =>
if(FileUtil.isText(bytes)){
val text = new String(bytes, "UTF-8")
val text = StringUtil.convertFromByteArray(bytes)
val lowerText = text.toLowerCase
val indices = keywords.map(lowerText.indexOf _)
if(!indices.exists(_ < 0)){
@@ -97,7 +98,7 @@ object RepositorySearchService {
val lineNumber = content.substring(0, indices.min).split("\n").size - 1
val highlightText = StringUtil.escapeHtml(content.split("\n").drop(lineNumber).take(5).mkString("\n"))
.replaceAll("(?i)(" + keywords.map("\\Q" + _ + "\\E").mkString("|") + ")",
"<span style=\"background-color: #ffff88;;\">$1</span>")
"<span class=\"highlight\">$1</span>")
(highlightText, lineNumber + 1)
} else {
(content.split("\n").take(5).mkString("\n"), 1)

View File

@@ -15,19 +15,27 @@ trait RepositoryService { self: AccountService =>
* @param userName the user name of the repository owner
* @param description the repository description
* @param isPrivate the repository type (private is true, otherwise false)
* @param originRepositoryName specify for the forked repository. (default is None)
* @param originUserName specify for the forked repository. (default is None)
*/
def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean): Unit = {
def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean,
originRepositoryName: Option[String] = None, originUserName: Option[String] = None,
parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None): Unit = {
Repositories insert
Repository(
userName = userName,
repositoryName = repositoryName,
isPrivate = isPrivate,
description = description,
defaultBranch = "master",
registeredDate = currentDate,
updatedDate = currentDate,
lastActivityDate = currentDate)
userName = userName,
repositoryName = repositoryName,
isPrivate = isPrivate,
description = description,
defaultBranch = "master",
registeredDate = currentDate,
updatedDate = currentDate,
lastActivityDate = currentDate,
originUserName = originUserName,
originRepositoryName = originRepositoryName,
parentUserName = parentUserName,
parentRepositoryName = parentRepositoryName)
IssueId insert (userName, repositoryName, 0)
}
@@ -39,8 +47,10 @@ trait RepositoryService { self: AccountService =>
Labels .filter(_.byRepository(userName, repositoryName)).delete
IssueComments .filter(_.byRepository(userName, repositoryName)).delete
Issues .filter(_.byRepository(userName, repositoryName)).delete
PullRequests .filter(_.byRepository(userName, repositoryName)).delete
IssueId .filter(_.byRepository(userName, repositoryName)).delete
Milestones .filter(_.byRepository(userName, repositoryName)).delete
WebHooks .filter(_.byRepository(userName, repositoryName)).delete
Repositories .filter(_.byRepository(userName, repositoryName)).delete
}
@@ -63,27 +73,42 @@ trait RepositoryService { self: AccountService =>
*/
def getRepository(userName: String, repositoryName: String, baseUrl: String): Option[RepositoryInfo] = {
(Query(Repositories) filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository =>
new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository)
// for getting issue count and pull request count
val issues = Query(Issues).filter { t =>
t.byRepository(repository.userName, repository.repositoryName) && (t.closed is false.bind)
}.map(_.pullRequest).list
new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
repository,
issues.size,
issues.filter(_ == true).size,
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
))
}
}
/**
* Returns the list of specified user's repositories.
* It contains own repositories and collaboration repositories.
*/
def getUserRepositories(userName: String, baseUrl: String): List[RepositoryInfo] = {
Query(Repositories).filter { t1 =>
(t1.userName is userName.bind) ||
(Query(Collaborators).filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName is userName.bind)} exists)
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository)
new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
repository,
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
))
}
}
/**
* Returns the list of visible repositories for the specified user.
* If repositoryUserName is given then filters results by repository owner.
*
*
* @param loginAccount the logged in account
* @param baseUrl the base url of this application
* @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
@@ -103,7 +128,13 @@ trait RepositoryService { self: AccountService =>
}).filter { t =>
repositoryUserName.map { userName => t.userName is userName.bind } getOrElse ConstColumn.TRUE
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository)
new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
repository,
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
))
}
}
@@ -170,17 +201,42 @@ trait RepositoryService { self: AccountService =>
}
}
// TODO It must be _.length instead of map (_.issueId) list).length.
// But it does not work on Slick 1.0.1 (worked on Slick 1.0.0).
// https://github.com/slick/slick/issues/170
private def getForkedCount(userName: String, repositoryName: String): Int =
Query(Repositories).filter { t =>
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
}.list.length
def getForkedRepositories(userName: String, repositoryName: String): List[String] =
Query(Repositories).filter { t =>
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
}
.sortBy(_.userName asc).map(_.userName).list
}
object RepositoryService {
case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository,
commitCount: Int, branchList: List[String], tags: List[util.JGitUtil.TagInfo]){
issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int,
branchList: List[String], tags: List[util.JGitUtil.TagInfo]){
def this(repo: JGitUtil.RepositoryInfo, model: Repository) = {
this(repo.owner, repo.name, repo.url, model, repo.commitCount, repo.branchList, repo.tags)
}
/**
* Creates instance with issue count and pull request count.
*/
def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int) =
this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags)
/**
* Creates instance without issue count and pull request count.
*/
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int) =
this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags)
}
case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode])
}

View File

@@ -1,6 +1,7 @@
package service
import model._
import service.SystemSettingsService.SystemSettings
/**
* This service is used for a view helper mainly.
@@ -10,6 +11,11 @@ import model._
*/
trait RequestCache {
def getSystemSettings()(implicit context: app.Context): SystemSettings =
context.cache("system_settings"){
new SystemSettingsService {}.loadSystemSettings()
}
def getIssue(userName: String, repositoryName: String, issueId: String)(implicit context: app.Context): Option[Issue] = {
context.cache(s"issue.${userName}/${repositoryName}#${issueId}"){
new IssuesService {}.getIssue(userName, repositoryName, issueId)

View File

@@ -1,40 +1,144 @@
package service
import util.Directory._
import SystemSettingsService._
trait SystemSettingsService {
def saveSystemSettings(settings: SystemSettings): Unit = {
val props = new java.util.Properties()
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
props.store(new java.io.FileOutputStream(GitBucketConf), null)
}
def loadSystemSettings(): SystemSettings = {
val props = new java.util.Properties()
if(GitBucketConf.exists){
props.load(new java.io.FileInputStream(GitBucketConf))
}
SystemSettings(getBoolean(props, "allow_account_registration"))
}
}
object SystemSettingsService {
case class SystemSettings(allowAccountRegistration: Boolean)
private val AllowAccountRegistration = "allow_account_registration"
private def getBoolean(props: java.util.Properties, key: String, default: Boolean = false): Boolean = {
val value = props.getProperty(key)
if(value == null || value.isEmpty){
default
} else {
value.toBoolean
}
}
}
package service
import util.Directory._
import util.ControlUtil._
import SystemSettingsService._
trait SystemSettingsService {
def saveSystemSettings(settings: SystemSettings): Unit = {
defining(new java.util.Properties()){ props =>
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
props.setProperty(Gravatar, settings.gravatar.toString)
props.setProperty(Notification, settings.notification.toString)
if(settings.notification) {
settings.smtp.foreach { smtp =>
props.setProperty(SmtpHost, smtp.host)
smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString))
smtp.user.foreach(props.setProperty(SmtpUser, _))
smtp.password.foreach(props.setProperty(SmtpPassword, _))
smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString))
}
}
props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString)
if(settings.ldapAuthentication){
settings.ldap.map { ldap =>
props.setProperty(LdapHost, ldap.host)
ldap.port.foreach(x => props.setProperty(LdapPort, x.toString))
ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x))
ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
props.setProperty(LdapBaseDN, ldap.baseDN)
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute)
}
}
props.store(new java.io.FileOutputStream(GitBucketConf), null)
}
}
def loadSystemSettings(): SystemSettings = {
defining(new java.util.Properties()){ props =>
if(GitBucketConf.exists){
props.load(new java.io.FileInputStream(GitBucketConf))
}
SystemSettings(
getValue(props, AllowAccountRegistration, false),
getValue(props, Gravatar, true),
getValue(props, Notification, false),
if(getValue(props, Notification, false)){
Some(Smtp(
getValue(props, SmtpHost, ""),
getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
getOptionValue(props, SmtpUser, None),
getOptionValue(props, SmtpPassword, None),
getOptionValue[Boolean](props, SmtpSsl, None)))
} else {
None
},
getValue(props, LdapAuthentication, false),
if(getValue(props, LdapAuthentication, false)){
Some(Ldap(
getValue(props, LdapHost, ""),
getOptionValue(props, LdapPort, Some(DefaultLdapPort)),
getOptionValue(props, LdapBindDN, None),
getOptionValue(props, LdapBindPassword, None),
getValue(props, LdapBaseDN, ""),
getValue(props, LdapUserNameAttribute, ""),
getValue(props, LdapMailAddressAttribute, "")))
} else {
None
}
)
}
}
}
object SystemSettingsService {
import scala.reflect.ClassTag
case class SystemSettings(
allowAccountRegistration: Boolean,
gravatar: Boolean,
notification: Boolean,
smtp: Option[Smtp],
ldapAuthentication: Boolean,
ldap: Option[Ldap])
case class Ldap(
host: String,
port: Option[Int],
bindDN: Option[String],
bindPassword: Option[String],
baseDN: String,
userNameAttribute: String,
mailAttribute: String)
case class Smtp(
host: String,
port: Option[Int],
user: Option[String],
password: Option[String],
ssl: Option[Boolean])
val DefaultSmtpPort = 25
val DefaultLdapPort = 389
private val AllowAccountRegistration = "allow_account_registration"
private val Gravatar = "gravatar"
private val Notification = "notification"
private val SmtpHost = "smtp.host"
private val SmtpPort = "smtp.port"
private val SmtpUser = "smtp.user"
private val SmtpPassword = "smtp.password"
private val SmtpSsl = "smtp.ssl"
private val LdapAuthentication = "ldap_authentication"
private val LdapHost = "ldap.host"
private val LdapPort = "ldap.port"
private val LdapBindDN = "ldap.bindDN"
private val LdapBindPassword = "ldap.bind_password"
private val LdapBaseDN = "ldap.baseDN"
private val LdapUserNameAttribute = "ldap.username_attribute"
private val LdapMailAddressAttribute = "ldap.mail_attribute"
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
defining(props.getProperty(key)){ value =>
if(value == null || value.isEmpty) default
else convertType(value).asInstanceOf[A]
}
private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] =
defining(props.getProperty(key)){ value =>
if(value == null || value.isEmpty) default
else Some(convertType(value)).asInstanceOf[Option[A]]
}
private def convertType[A: ClassTag](value: String) =
defining(implicitly[ClassTag[A]].runtimeClass){ c =>
if(c == classOf[Boolean]) value.toBoolean
else if(c == classOf[Int]) value.toInt
else value
}
}

View File

@@ -0,0 +1,145 @@
package service
import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession
import model._
import org.slf4j.LoggerFactory
import service.RepositoryService.RepositoryInfo
import util.JGitUtil
import org.eclipse.jgit.diff.DiffEntry
import util.JGitUtil.CommitInfo
import org.eclipse.jgit.api.Git
import org.apache.http.message.BasicNameValuePair
import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.protocol.HTTP
import org.apache.http.NameValuePair
trait WebHookService {
import WebHookService._
private val logger = LoggerFactory.getLogger(classOf[WebHookService])
def getWebHookURLs(owner: String, repository: String): List[WebHook] =
Query(WebHooks).filter(_.byRepository(owner, repository)).sortBy(_.url).list
def addWebHookURL(owner: String, repository: String, url :String): Unit =
WebHooks.insert(WebHook(owner, repository, url))
def deleteWebHookURL(owner: String, repository: String, url :String): Unit =
Query(WebHooks).filter(_.byPrimaryKey(owner, repository, url)).delete
def callWebHook(owner: String, repository: String, payload: WebHookPayload): Unit = {
import org.json4s._
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.{read, write}
import org.apache.http.client.methods.HttpPost
import org.apache.http.impl.client.DefaultHttpClient
import scala.concurrent._
import ExecutionContext.Implicits.global
logger.debug("start callWebHook")
implicit val formats = Serialization.formats(NoTypeHints)
val webHookURLs = getWebHookURLs(owner, repository)
if(webHookURLs.nonEmpty){
val json = write(payload)
val httpClient = new DefaultHttpClient()
webHookURLs.foreach { webHookUrl =>
val f = future {
logger.debug(s"start web hook invocation for ${webHookUrl}")
val httpPost = new HttpPost(webHookUrl.url)
val params: java.util.List[NameValuePair] = new java.util.ArrayList()
params.add(new BasicNameValuePair("payload", json))
httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8"))
httpClient.execute(httpPost)
httpPost.releaseConnection()
logger.debug(s"end web hook invocation for ${webHookUrl}")
}
f.onSuccess {
case s => logger.debug(s"Success: web hook request to ${webHookUrl.url}")
}
f.onFailure {
case t => logger.error(s"Failed: web hook request to ${webHookUrl.url}", t)
}
}
}
logger.debug("end callWebHook")
}
}
object WebHookService {
case class WebHookPayload(
ref: String,
commits: List[WebHookCommit],
repository: WebHookRepository)
object WebHookPayload {
def apply(git: Git, refName: String, repositoryInfo: RepositoryInfo,
commits: List[CommitInfo], repositoryOwner: Account): WebHookPayload =
WebHookPayload(
refName,
commits.map { commit =>
val diffs = JGitUtil.getDiffs(git, commit.id, false)
val commitUrl = repositoryInfo.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/commit/" + commit.id
WebHookCommit(
id = commit.id,
message = commit.fullMessage,
timestamp = commit.time.toString,
url = commitUrl,
added = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.ADD) => x.newPath },
removed = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.DELETE) => x.oldPath },
modified = diffs._1.collect { case x if(x.changeType != DiffEntry.ChangeType.ADD &&
x.changeType != DiffEntry.ChangeType.DELETE) => x.newPath },
author = WebHookUser(
name = commit.committer,
email = commit.mailAddress
)
)
}.toList,
WebHookRepository(
name = repositoryInfo.name,
url = repositoryInfo.url,
description = repositoryInfo.repository.description.getOrElse(""),
watchers = 0,
forks = repositoryInfo.forkedCount,
`private` = repositoryInfo.repository.isPrivate,
owner = WebHookUser(
name = repositoryOwner.userName,
email = repositoryOwner.mailAddress
)
)
)
}
case class WebHookCommit(
id: String,
message: String,
timestamp: String,
url: String,
added: List[String],
removed: List[String],
modified: List[String],
author: WebHookUser)
case class WebHookRepository(
name: String,
url: String,
description: String,
watchers: Int,
forks: Int,
`private`: Boolean,
owner: WebHookUser)
case class WebHookUser(
name: String,
email: String)
}

View File

@@ -4,10 +4,8 @@ import java.io.File
import java.util.Date
import org.eclipse.jgit.api.Git
import org.apache.commons.io.FileUtils
import util.JGitUtil.DiffInfo
import util.{Directory, JGitUtil}
import org.eclipse.jgit.treewalk.CanonicalTreeParser
import java.util.concurrent.ConcurrentHashMap
import util.{Directory, JGitUtil, LockUtil}
import util.ControlUtil._
object WikiService {
@@ -31,72 +29,45 @@ object WikiService {
*/
case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date)
/**
* lock objects
*/
private val locks = new ConcurrentHashMap[String, AnyRef]()
/**
* Returns the lock object for the specified repository.
*/
private def getLockObject(owner: String, repository: String): AnyRef = synchronized {
val key = owner + "/" + repository
if(!locks.containsKey(key)){
locks.put(key, new AnyRef())
}
locks.get(key)
}
/**
* Synchronizes a given function which modifies the working copy of the wiki repository.
*
* @param owner the repository owner
* @param repository the repository name
* @param f the function which modifies the working copy of the wiki repository
* @tparam T the return type of the given function
* @return the result of the given function
*/
def lock[T](owner: String, repository: String)(f: => T): T = getLockObject(owner, repository).synchronized(f)
}
trait WikiService {
import WikiService._
def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit = {
lock(owner, repository){
val dir = Directory.getWikiRepositoryDir(owner, repository)
if(!dir.exists){
try {
JGitUtil.initRepository(dir)
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit")
} finally {
// once delete cloned repository because initial cloned repository does not have 'branch.master.merge'
FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner, repository))
def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit =
LockUtil.lock(s"${owner}/${repository}/wiki"){
defining(Directory.getWikiRepositoryDir(owner, repository)){ dir =>
if(!dir.exists){
try {
JGitUtil.initRepository(dir)
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit")
} finally {
// once delete cloned repository because initial cloned repository does not have 'branch.master.merge'
FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner, repository))
}
}
}
}
}
/**
* Returns the wiki page.
*/
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git =>
if(!JGitUtil.isEmpty(git)){
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
optionIf(!JGitUtil.isEmpty(git)){
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
WikiPageInfo(file.name, new String(git.getRepository.open(file.id).getBytes, "UTF-8"), file.committer, file.time)
}
} else None
}
}
}
/**
* Returns the content of the specified file.
*/
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] = {
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git =>
if(!JGitUtil.isEmpty(git)){
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] =
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
optionIf(!JGitUtil.isEmpty(git)){
val index = path.lastIndexOf('/')
val parentPath = if(index < 0) "." else path.substring(0, index)
val fileName = if(index < 0) path else path.substring(index + 1)
@@ -104,15 +75,14 @@ trait WikiService {
JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file =>
git.getRepository.open(file.id).getBytes
}
} else None
}
}
}
/**
* Returns the list of wiki page names.
*/
def getWikiPageList(owner: String, repository: String): List[String] = {
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git =>
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
JGitUtil.getFileList(git, "master", ".")
.filter(_.name.endsWith(".md"))
.map(_.name.replaceFirst("\\.md$", ""))
@@ -124,36 +94,34 @@ trait WikiService {
* Save the wiki page.
*/
def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
content: String, committer: model.Account, message: String): Unit = {
content: String, committer: model.Account, message: String): Option[String] = {
lock(owner, repository){
// clone working copy
val workDir = Directory.getWikiWorkDir(owner, repository)
cloneOrPullWorkingCopy(workDir, owner, repository)
LockUtil.lock(s"${owner}/${repository}/wiki"){
defining(Directory.getWikiWorkDir(owner, repository)){ workDir =>
// clone working copy
cloneOrPullWorkingCopy(workDir, owner, repository)
// write as file
JGitUtil.withGit(workDir){ git =>
val file = new File(workDir, newPageName + ".md")
val added = if(!file.exists || FileUtils.readFileToString(file, "UTF-8") != content){
FileUtils.writeStringToFile(file, content, "UTF-8")
git.add.addFilepattern(file.getName).call
true
} else {
false
}
// write as file
using(Git.open(workDir)){ git =>
defining(new File(workDir, newPageName + ".md")){ file =>
val added = executeIf(!file.exists || FileUtils.readFileToString(file, "UTF-8") != content){
FileUtils.writeStringToFile(file, content, "UTF-8")
git.add.addFilepattern(file.getName).call
}
// delete file
val deleted = if(currentPageName != "" && currentPageName != newPageName){
git.rm.addFilepattern(currentPageName + ".md").call
true
} else {
false
}
// delete file
val deleted = executeIf(currentPageName != "" && currentPageName != newPageName){
git.rm.addFilepattern(currentPageName + ".md").call
}
// commit and push
if(added || deleted){
git.commit.setCommitter(committer.userName, committer.mailAddress).setMessage(message).call
git.push.call
// commit and push
optionIf(added || deleted){
defining(git.commit.setCommitter(committer.userName, committer.mailAddress).setMessage(message).call){ commit =>
git.push.call
Some(commit.getName)
}
}
}
}
}
}
@@ -162,59 +130,37 @@ trait WikiService {
/**
* Delete the wiki page.
*/
def deleteWikiPage(owner: String, repository: String, pageName: String, committer: String, message: String): Unit = {
lock(owner, repository){
// clone working copy
val workDir = Directory.getWikiWorkDir(owner, repository)
cloneOrPullWorkingCopy(workDir, owner, repository)
def deleteWikiPage(owner: String, repository: String, pageName: String,
committer: String, mailAddress: String, message: String): Unit = {
LockUtil.lock(s"${owner}/${repository}/wiki"){
defining(Directory.getWikiWorkDir(owner, repository)){ workDir =>
// clone working copy
cloneOrPullWorkingCopy(workDir, owner, repository)
// delete file
new File(workDir, pageName + ".md").delete
JGitUtil.withGit(workDir){ git =>
git.rm.addFilepattern(pageName + ".md").call
// commit and push
// TODO committer's mail address
git.commit.setAuthor(committer, committer + "@devnull").setMessage(message).call
git.push.call
// delete file
new File(workDir, pageName + ".md").delete
using(Git.open(workDir)){ git =>
git.rm.addFilepattern(pageName + ".md").call
// commit and push
git.commit.setAuthor(committer, mailAddress).setMessage(message).call
git.push.call
}
}
}
}
/**
* Returns differences between specified commits.
*/
def getWikiDiffs(git: Git, commitId1: String, commitId2: String): List[DiffInfo] = {
// get diff between specified commit and its previous commit
val reader = git.getRepository.newObjectReader
val oldTreeIter = new CanonicalTreeParser
oldTreeIter.reset(reader, git.getRepository.resolve(commitId1 + "^{tree}"))
val newTreeIter = new CanonicalTreeParser
newTreeIter.reset(reader, git.getRepository.resolve(commitId2 + "^{tree}"))
import scala.collection.JavaConverters._
git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff =>
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
JGitUtil.getContent(git, diff.getOldId.toObjectId, false).map(new String(_, "UTF-8")),
JGitUtil.getContent(git, diff.getNewId.toObjectId, false).map(new String(_, "UTF-8")))
}.toList
}
private def cloneOrPullWorkingCopy(workDir: File, owner: String, repository: String): Unit = {
if(!workDir.exists){
val git =
Git.cloneRepository
.setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString)
.setDirectory(workDir)
.call
git.getRepository.close // close .git resources.
} else {
JGitUtil.withGit(workDir){ git =>
git.pull.call
}
Git.cloneRepository
.setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString)
.setDirectory(workDir)
.call
.getRepository
.close
} else using(Git.open(workDir)){ git =>
git.pull.call
}
}

View File

@@ -6,7 +6,9 @@ import org.apache.commons.io.FileUtils
import javax.servlet.ServletContextEvent
import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory
import util.Directory
import util.Directory._
import util.ControlUtil._
import org.eclipse.jgit.api.Git
object AutoUpdate {
@@ -26,15 +28,14 @@ object AutoUpdate {
*/
def update(conn: Connection): Unit = {
val sqlPath = s"update/${majorVersion}_${minorVersion}.sql"
val in = Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)
if(in != null){
val sql = IOUtils.toString(in, "UTF-8")
val stmt = conn.createStatement()
try {
logger.debug(sqlPath + "=" + sql)
stmt.executeUpdate(sql)
} finally {
stmt.close()
using(Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)){ in =>
if(in != null){
val sql = IOUtils.toString(in, "UTF-8")
using(conn.createStatement()){ stmt =>
logger.debug(sqlPath + "=" + sql)
stmt.executeUpdate(sql)
}
}
}
}
@@ -49,21 +50,24 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
Version(1, 6),
Version(1, 5),
Version(1, 4),
new Version(1, 3){
override def update(conn: Connection): Unit = {
super.update(conn)
// Fix wiki repository configuration
val rs = conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")
while(rs.next){
val wikidir = Directory.getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))
val repository = org.eclipse.jgit.api.Git.open(wikidir).getRepository
val config = repository.getConfig
if(!config.getBoolean("http", "receivepack", false)){
config.setBoolean("http", null, "receivepack", true)
config.save
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
}
}
}
}
repository.close
}
}
},
@@ -80,7 +84,7 @@ object AutoUpdate {
/**
* The version file (GITBUCKET_HOME/version).
*/
val versionFile = new File(Directory.GitBucketHome, "version")
val versionFile = new File(GitBucketHome, "version")
/**
* Returns the current version from the version file.
@@ -111,26 +115,29 @@ class AutoUpdateListener extends org.h2.server.web.DbStarter {
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
override def contextInitialized(event: ServletContextEvent): Unit = {
event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome}")
super.contextInitialized(event)
logger.debug("H2 started")
logger.debug("Start schema update")
val conn = getConnection()
try {
val currentVersion = getCurrentVersion()
if(currentVersion == headVersion){
logger.debug("No update")
} else {
versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
conn.commit()
logger.debug("Updated from " + currentVersion.versionString + " to " + headVersion.versionString)
}
} catch {
case ex: Throwable => {
logger.error("Failed to schema update", ex)
ex.printStackTrace()
conn.rollback()
defining(getConnection()){ conn =>
try {
defining(getCurrentVersion()){ currentVersion =>
if(currentVersion == headVersion){
logger.debug("No update")
} else {
versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
conn.commit()
logger.debug("Updated from " + currentVersion.versionString + " to " + headVersion.versionString)
}
}
} catch {
case ex: Throwable => {
logger.error("Failed to schema update", ex)
ex.printStackTrace()
conn.rollback()
}
}
}
logger.debug("End schema update")

View File

@@ -2,14 +2,16 @@ package servlet
import javax.servlet._
import javax.servlet.http._
import util.StringUtil._
import service.{AccountService, RepositoryService}
import service.{SystemSettingsService, AccountService, RepositoryService}
import org.slf4j.LoggerFactory
import util.Implicits._
import util.ControlUtil._
import util.Keys
/**
* Provides BASIC Authentication for [[servlet.GitRepositoryServlet]].
*/
class BasicAuthenticationFilter extends Filter with RepositoryService with AccountService {
class BasicAuthenticationFilter extends Filter with RepositoryService with AccountService with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[BasicAuthenticationFilter])
@@ -26,29 +28,30 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
}
try {
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
val repositoryOwner = paths(2)
val repositoryName = paths(3).replaceFirst("\\.git$", "")
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki", ""), "") match {
case Some(repository) => {
if(!request.getRequestURI.endsWith("/git-receive-pack") &&
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) if(isWritableUser(username, password, repository)) => {
request.setAttribute("USER_NAME", username)
chain.doFilter(req, wrappedResponse)
chain.doFilter(req, wrappedResponse)
} else {
request.getHeader("Authorization") match {
case null => requireAuth(response)
case auth => decodeAuthHeader(auth).split(":") match {
case Array(username, password) if(isWritableUser(username, password, repository)) => {
request.setAttribute(Keys.Request.UserName, username)
chain.doFilter(req, wrappedResponse)
}
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 => response.sendError(HttpServletResponse.SC_NOT_FOUND)
}
} catch {
case ex: Exception => {
@@ -58,12 +61,12 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
}
}
private def isWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Boolean = {
getAccountByUserName(username).map { account =>
account.password == sha1(password) && hasWritePermission(repository.owner, repository.name, Some(account))
} getOrElse false
}
private def isWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Boolean =
authenticate(loadSystemSettings(), username, password) match {
case Some(account) => hasWritePermission(repository.owner, repository.name, Some(account))
case None => false
}
private def requireAuth(response: HttpServletResponse): Unit = {
response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"")
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)

View File

@@ -9,8 +9,12 @@ import org.slf4j.LoggerFactory
import javax.servlet.ServletConfig
import javax.servlet.ServletContext
import javax.servlet.http.HttpServletRequest
import util.{JGitUtil, Directory}
import util.{Keys, JGitUtil, Directory}
import util.ControlUtil._
import util.Implicits._
import service._
import WebHookService._
import org.eclipse.jgit.api.Git
/**
* Provides Git repository via HTTP.
@@ -24,7 +28,7 @@ class GitRepositoryServlet extends GitServlet {
override def init(config: ServletConfig): Unit = {
setReceivePackFactory(new GitBucketReceivePackFactory())
// TODO are there any other ways...?
super.init(new ServletConfig(){
def getInitParameter(name: String): String = name match {
@@ -33,12 +37,14 @@ class GitRepositoryServlet extends GitServlet {
case name => config.getInitParameter(name)
}
def getInitParameterNames(): java.util.Enumeration[String] = {
config.getInitParameterNames
config.getInitParameterNames
}
def getServletContext(): ServletContext = config.getServletContext
def getServletName(): String = config.getServletName
});
super.init(config)
}
}
@@ -49,31 +55,34 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
val receivePack = new ReceivePack(db)
val userName = request.getAttribute("USER_NAME").asInstanceOf[String]
val userName = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
logger.debug("requestURI: " + request.getRequestURI)
logger.debug("userName:" + userName)
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
val owner = paths(2)
val repository = paths(3).replaceFirst("\\.git$", "")
logger.debug("repository:" + owner + "/" + repository)
defining(request.paths){ paths =>
val owner = paths(1)
val repository = paths(2).replaceFirst("\\.git$", "")
val baseURL = request.getRequestURL.toString.replaceFirst("/git/.*", "")
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, userName))
receivePack
logger.debug("repository:" + owner + "/" + repository)
logger.debug("baseURL:" + baseURL)
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, userName, baseURL))
receivePack
}
}
}
import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String, userName: String) extends PostReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService {
class CommitLogHook(owner: String, repository: String, userName: String, baseURL: String) extends PostReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService with WebHookService {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
JGitUtil.withGit(Directory.getRepositoryDir(owner, repository)) { git =>
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
commands.asScala.foreach { command =>
val commits = JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
val refName = command.getRefName.split("/")
@@ -108,6 +117,16 @@ class CommitLogHook(owner: String, repository: String, userName: String) extends
case _ =>
}
}
// call web hook
val payload = WebHookPayload(
git,
command.getRefName,
getRepository(owner, repository, baseURL).get,
newCommits,
getAccountByUserName(owner).get)
callWebHook(owner, repository, payload)
}
}
// update repository last modified time.

View File

@@ -3,7 +3,6 @@ package servlet
import javax.servlet._
import org.slf4j.LoggerFactory
import javax.servlet.http.HttpServletRequest
import scala.slick.session.Database
/**
* Controls the transaction with the open session in view pattern.
@@ -21,15 +20,19 @@ class TransactionFilter extends Filter {
// assets don't need transaction
chain.doFilter(req, res)
} else {
val context = req.getServletContext
Database.forURL(context.getInitParameter("db.url"),
context.getInitParameter("db.user"),
context.getInitParameter("db.password")) withTransaction {
logger.debug("TODO begin transaction")
Database(req.getServletContext) withTransaction {
logger.debug("begin transaction")
chain.doFilter(req, res)
logger.debug("TODO end transaction")
logger.debug("end transaction")
}
}
}
}
}
object Database {
def apply(context: ServletContext): scala.slick.session.Database =
scala.slick.session.Database.forURL(context.getInitParameter("db.url"),
context.getInitParameter("db.user"),
context.getInitParameter("db.password"))
}

View File

@@ -3,6 +3,8 @@ package util
import app.ControllerBase
import service._
import RepositoryService.RepositoryInfo
import util.Implicits._
import util.ControlUtil._
/**
* Allows only oneself and administrators.
@@ -13,11 +15,12 @@ trait OneselfAuthenticator { self: ControllerBase =>
private def authenticate(action: => Any) = {
{
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
context.loginAccount match {
case Some(x) if(x.isAdmin) => action
case Some(x) if(paths(1) == x.userName) => action
case _ => Unauthorized()
defining(request.paths){ paths =>
context.loginAccount match {
case Some(x) if(x.isAdmin) => action
case Some(x) if(paths(0) == x.userName) => action
case _ => Unauthorized()
}
}
}
}
@@ -32,14 +35,15 @@ trait OwnerAuthenticator { self: ControllerBase with RepositoryService =>
private def authenticate(action: (RepositoryInfo) => Any) = {
{
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
getRepository(paths(1), paths(2), baseUrl).map { repository =>
context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(repository.owner == x.userName) => action(repository)
case _ => Unauthorized()
}
} getOrElse NotFound()
defining(request.paths){ paths =>
getRepository(paths(0), paths(1), baseUrl).map { repository =>
context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(repository.owner == x.userName) => action(repository)
case _ => Unauthorized()
}
} getOrElse NotFound()
}
}
}
}
@@ -87,15 +91,16 @@ trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService =
private def authenticate(action: (RepositoryInfo) => Any) = {
{
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
getRepository(paths(1), paths(2), baseUrl).map { repository =>
context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(paths(1) == x.userName) => action(repository)
case Some(x) if(getCollaborators(paths(1), paths(2)).contains(x.userName)) => action(repository)
case _ => Unauthorized()
}
} getOrElse NotFound()
defining(request.paths){ paths =>
getRepository(paths(0), paths(1), baseUrl).map { repository =>
context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(paths(0) == x.userName) => action(repository)
case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository)
case _ => Unauthorized()
}
} getOrElse NotFound()
}
}
}
}
@@ -109,19 +114,20 @@ trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
private def authenticate(action: (RepositoryInfo) => Any) = {
{
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
getRepository(paths(1), paths(2), baseUrl).map { repository =>
if(!repository.repository.isPrivate){
action(repository)
} else {
context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(paths(1) == x.userName) => action(repository)
case Some(x) if(getCollaborators(paths(1), paths(2)).contains(x.userName)) => action(repository)
case _ => Unauthorized()
defining(request.paths){ paths =>
getRepository(paths(0), paths(1), baseUrl).map { repository =>
if(!repository.repository.isPrivate){
action(repository)
} else {
context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(paths(0) == x.userName) => action(repository)
case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository)
case _ => Unauthorized()
}
}
}
} getOrElse NotFound()
} getOrElse NotFound()
}
}
}
}
@@ -135,16 +141,17 @@ trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService =
private def authenticate(action: (RepositoryInfo) => Any) = {
{
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
getRepository(paths(1), paths(2), baseUrl).map { repository =>
context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(!repository.repository.isPrivate) => action(repository)
case Some(x) if(paths(1) == x.userName) => action(repository)
case Some(x) if(getCollaborators(paths(1), paths(2)).contains(x.userName)) => action(repository)
case _ => Unauthorized()
}
} getOrElse NotFound()
defining(request.paths){ paths =>
getRepository(paths(0), paths(1), baseUrl).map { repository =>
context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(!repository.repository.isPrivate) => action(repository)
case Some(x) if(paths(0) == x.userName) => action(repository)
case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository)
case _ => Unauthorized()
}
} getOrElse NotFound()
}
}
}
}

View File

@@ -0,0 +1,48 @@
package util
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.TreeWalk
/**
* Provides control facilities.
*/
object ControlUtil {
def defining[A, B](value: A)(f: A => B): B = f(value)
def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B =
try f(resource) finally {
if(resource != null){
try {
resource.close()
} catch {
case e: Throwable => // ignore
}
}
}
def using[T](git: Git)(f: Git => T): T =
try f(git) finally git.getRepository.close
def using[T](git1: Git, git2: Git)(f: (Git, Git) => T): T =
try f(git1, git2) finally {
git1.getRepository.close
git2.getRepository.close
}
def using[T](revWalk: RevWalk)(f: RevWalk => T): T =
try f(revWalk) finally revWalk.release()
def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T =
try f(treeWalk) finally treeWalk.release()
def executeIf(condition: => Boolean)(action: => Unit): Boolean =
if(condition){
action
true
} else false
def optionIf[T](condition: => Boolean)(action: => Option[T]): Option[T] =
if(condition) action else None
}

View File

@@ -1,32 +1,38 @@
package util
import java.io.File
import util.ControlUtil._
/**
* Provides directories used by GitBucket.
*/
object Directory {
val GitBucketHome = new File(System.getProperty("user.home"), "gitbucket").getAbsolutePath
val GitBucketHome = (scala.util.Properties.envOrNone("GITBUCKET_HOME") match {
case Some(env) => new File(env)
case None => new File(System.getProperty("user.home"), "gitbucket")
}).getAbsolutePath
val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
val RepositoryHome = s"${GitBucketHome}/repositories"
val DatabaseHome = s"${GitBucketHome}/data"
/**
* Repository names of the specified user.
*/
def getRepositories(owner: String): List[String] = {
val dir = new File(s"${RepositoryHome}/${owner}")
if(dir.exists){
dir.listFiles.filter { file =>
file.isDirectory && !file.getName.endsWith(".wiki.git")
}.map(_.getName.replaceFirst("\\.git$", "")).toList
} else {
Nil
def getRepositories(owner: String): List[String] =
defining(new File(s"${RepositoryHome}/${owner}")){ dir =>
if(dir.exists){
dir.listFiles.filter { file =>
file.isDirectory && !file.getName.endsWith(".wiki.git")
}.map(_.getName.replaceFirst("\\.git$", "")).toList
} else {
Nil
}
}
}
/**
* Substance directory of the repository.
*/

View File

@@ -4,19 +4,18 @@ import org.apache.commons.io.{IOUtils, FileUtils}
import java.net.URLConnection
import java.io.File
import org.apache.commons.compress.archivers.zip.{ZipArchiveEntry, ZipArchiveOutputStream}
import util.ControlUtil._
object FileUtil {
def getMimeType(name: String): String = {
val fileNameMap = URLConnection.getFileNameMap()
val mimeType = fileNameMap.getContentTypeFor(name)
if(mimeType == null){
"application/octeat-stream"
} else {
mimeType
def getMimeType(name: String): String =
defining(URLConnection.getFileNameMap()){ fileNameMap =>
fileNameMap.getContentTypeFor(name) match {
case null => "application/octet-stream"
case mimeType => mimeType
}
}
}
def isImage(name: String): Boolean = getMimeType(name).startsWith("image/")
def isLarge(size: Long): Boolean = (size > 1024 * 1000)
@@ -36,21 +35,19 @@ object FileUtil {
}
}
val out = new ZipArchiveOutputStream(dest)
try {
using(new ZipArchiveOutputStream(dest)){ out =>
addDirectoryToZip(out, dir, dir.getName)
} finally {
IOUtils.closeQuietly(out)
}
}
def getExtension(name: String): String = {
val index = name.lastIndexOf('.')
if(index >= 0){
name.substring(index + 1)
} else {
""
}
def getFileName(path: String): String = defining(path.lastIndexOf('/')){ i =>
if(i >= 0) path.substring(i + 1) else path
}
def getExtension(name: String): String =
name.lastIndexOf('.') match {
case i if(i >= 0) => name.substring(i + 1)
case _ => ""
}
}

View File

@@ -1,6 +1,7 @@
package util
import scala.util.matching.Regex
import javax.servlet.http.{HttpSession, HttpServletRequest}
/**
* Provides some usable implicit conversions.
@@ -12,7 +13,7 @@ object Implicits {
def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition)
@scala.annotation.tailrec
private def split[A](list: Seq[A], result: Seq[Seq[A]] = Nil)(condition: (A, A) => Boolean): Seq[Seq[A]] = {
private def split[A](list: Seq[A], result: Seq[Seq[A]] = Nil)(condition: (A, A) => Boolean): Seq[Seq[A]] =
list match {
case x :: xs => {
xs.span(condition(x, _)) match {
@@ -21,7 +22,6 @@ object Implicits {
}
case Nil => result
}
}
}
implicit class RichString(value: String){
@@ -43,4 +43,30 @@ object Implicits {
}
}
implicit class RichRequest(request: HttpServletRequest){
def paths: Array[String] = request.getRequestURI.substring(request.getContextPath.length + 1).split("/")
def hasQueryString: Boolean = request.getQueryString != null
def hasAttribute(name: String): Boolean = request.getAttribute(name) != null
}
implicit class RichSession(session: HttpSession){
def putAndGet[T](key: String, value: T): T = {
session.setAttribute(key, value)
value
}
def getAndRemove[T](key: String): Option[T] = {
val value = session.getAttribute(key).asInstanceOf[T]
if(value == null){
session.removeAttribute(key)
}
Option(value)
}
}
}

View File

@@ -2,19 +2,19 @@ package util
import org.eclipse.jgit.api.Git
import util.Directory._
import util.StringUtil._
import util.ControlUtil._
import scala.collection.JavaConverters._
import javax.servlet.ServletContext
import org.eclipse.jgit.lib._
import org.eclipse.jgit.revwalk._
import org.eclipse.jgit.revwalk.filter._
import org.eclipse.jgit.treewalk._
import org.eclipse.jgit.treewalk.filter._
import org.eclipse.jgit.diff._
import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.eclipse.jgit.util.io.DisabledOutputStream
import org.eclipse.jgit.errors.MissingObjectException
import java.util.Date
import org.eclipse.jgit.api.errors.NoHeadException
import service.RepositoryService
/**
* Provides complex JGit operations.
@@ -69,29 +69,17 @@ object JGitUtil {
rev.getFullMessage,
rev.getParents().map(_.name).toList)
val summary = {
val i = fullMessage.trim.indexOf("\n")
val firstLine = if(i >= 0){
fullMessage.trim.substring(0, i).trim
} else {
fullMessage
}
if(firstLine.length > shortMessage.length){
shortMessage
} else {
firstLine
val summary = defining(fullMessage.trim.indexOf("\n")){ i =>
defining(if(i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage){ firstLine =>
if(firstLine.length > shortMessage.length) shortMessage else firstLine
}
}
val description = {
val i = fullMessage.trim.indexOf("\n")
if(i >= 0){
val description = defining(fullMessage.trim.indexOf("\n")){ i =>
optionIf(i >= 0){
Some(fullMessage.trim.substring(i).trim)
} else {
None
}
}
}
case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String])
@@ -114,33 +102,18 @@ object JGitUtil {
case class TagInfo(name: String, time: Date, id: String)
/**
* Use this method to use the Git object.
* Repository resources are released certainly after processing.
*/
def withGit[T](dir: java.io.File)(f: Git => T): T = withGit(Git.open(dir))(f)
/**
* Use this method to use the Git object.
* Repository resources are released certainly after processing.
*/
def withGit[T](git: Git)(f: Git => T): T = {
try {
f(git)
} finally {
git.getRepository.close
}
}
/**
* Returns RevCommit from the commit id.
* Returns RevCommit from the commit or tag id.
*
* @param git the Git object
* @param commitId the ObjectId of the commit
* @return the RevCommit for the specified commit
* @param objectId the ObjectId of the commit or tag
* @return the RevCommit for the specified commit or tag
*/
def getRevCommitFromId(git: Git, commitId: ObjectId): RevCommit = {
def getRevCommitFromId(git: Git, objectId: ObjectId): RevCommit = {
val revWalk = new RevWalk(git.getRepository)
val revCommit = revWalk.parseCommit(commitId)
val revCommit = revWalk.parseAny(objectId) match {
case r: RevTag => revWalk.parseCommit(r.getObject)
case _ => revWalk.parseCommit(objectId)
}
revWalk.dispose
revCommit
}
@@ -149,15 +122,10 @@ object JGitUtil {
* Returns the repository information. It contains branch names and tag names.
*/
def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = {
withGit(getRepositoryDir(owner, repository)){ git =>
using(Git.open(getRepositoryDir(owner, repository))){ git =>
try {
// get commit count
val i = git.log.all.call.iterator
var commitCount = 0
while(i.hasNext && commitCount <= 1000){
i.next
commitCount = commitCount + 1
}
val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(1000).sum
RepositoryInfo(
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git",
@@ -191,45 +159,43 @@ object JGitUtil {
* @return HTML of the file list
*/
def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = {
val revWalk = new RevWalk(git.getRepository)
val objectId = git.getRepository.resolve(revision)
val revCommit = revWalk.parseCommit(objectId)
val treeWalk = new TreeWalk(git.getRepository)
treeWalk.addTree(revCommit.getTree)
if(path != "."){
treeWalk.setRecursive(true)
treeWalk.setFilter(new TreeFilter(){
var stopRecursive = false
def include(walker: TreeWalk): Boolean = {
val targetPath = walker.getPathString
if((path + "/").startsWith(targetPath)){
true
} else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf("/") < 0){
stopRecursive = true
treeWalk.setRecursive(false)
true
} else {
false
}
}
def shouldBeRecursive(): Boolean = !stopRecursive
override def clone: TreeFilter = return this
})
}
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String)]
while (treeWalk.next()) {
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString))
using(new RevWalk(git.getRepository)){ revWalk =>
val objectId = git.getRepository.resolve(revision)
val revCommit = revWalk.parseCommit(objectId)
using(new TreeWalk(git.getRepository)){ treeWalk =>
treeWalk.addTree(revCommit.getTree)
if(path != "."){
treeWalk.setRecursive(true)
treeWalk.setFilter(new TreeFilter(){
var stopRecursive = false
def include(walker: TreeWalk): Boolean = {
val targetPath = walker.getPathString
if((path + "/").startsWith(targetPath)){
true
} else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf("/") < 0){
stopRecursive = true
treeWalk.setRecursive(false)
true
} else {
false
}
}
def shouldBeRecursive(): Boolean = !stopRecursive
override def clone: TreeFilter = return this
})
}
while (treeWalk.next()) {
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString))
}
}
}
treeWalk.release
revWalk.dispose
val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision)
list.map { case (objectId, fileMode, path, name) =>
@@ -258,7 +224,7 @@ object JGitUtil {
* @param page the page number (1-)
* @param limit the number of commit info per page. 0 (default) means unlimited.
* @param path filters by this path. default is no filter.
* @return a tuple of the commit list and whether has next
* @return a tuple of the commit list and whether has next, or the error message
*/
def getCommitLog(git: Git, revision: String, page: Int = 1, limit: Int = 0, path: String = ""): Either[String, (List[CommitInfo], Boolean)] = {
val fixedPage = if(page <= 0) 1 else page
@@ -273,27 +239,48 @@ object JGitUtil {
case _ => (logs, i.hasNext)
}
val revWalk = new RevWalk(git.getRepository)
val objectId = git.getRepository.resolve(revision)
if(objectId == null){
Left(s"${revision} can't be resolved.")
} else {
revWalk.markStart(revWalk.parseCommit(objectId))
if(path.nonEmpty){
revWalk.setRevFilter(new RevFilter(){
def include(walk: RevWalk, commit: RevCommit): Boolean = {
getDiffs(git, commit.getName, false).find(_.newPath == path).nonEmpty
using(new RevWalk(git.getRepository)){ revWalk =>
defining(git.getRepository.resolve(revision)){ objectId =>
if(objectId == null){
Left(s"${revision} can't be resolved.")
} else {
revWalk.markStart(revWalk.parseCommit(objectId))
if(path.nonEmpty){
revWalk.setRevFilter(new RevFilter(){
def include(walk: RevWalk, commit: RevCommit): Boolean = {
getDiffs(git, commit.getName, false)._1.find(_.newPath == path).nonEmpty
}
override def clone(): RevFilter = this
})
}
override def clone(): RevFilter = this
})
Right(getCommitLog(revWalk.iterator, 0, Nil))
}
}
val commits = getCommitLog(revWalk.iterator, 0, Nil)
revWalk.release
Right(commits)
}
}
def getCommitLogs(git: Git, begin: String, includesLastCommit: Boolean = false)
(endCondition: RevCommit => Boolean): List[CommitInfo] = {
@scala.annotation.tailrec
def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[CommitInfo]): List[CommitInfo] =
i.hasNext match {
case true => {
val revCommit = i.next
if(endCondition(revCommit)){
if(includesLastCommit) logs :+ new CommitInfo(revCommit) else logs
} else {
getCommitLog(i, logs :+ new CommitInfo(revCommit))
}
}
case false => logs
}
using(new RevWalk(git.getRepository)){ revWalk =>
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(begin)))
getCommitLog(revWalk.iterator, Nil).reverse
}
}
/**
* Returns the commit list between two revisions.
@@ -303,30 +290,9 @@ object JGitUtil {
* @param to the to revision
* @return the commit list
*/
def getCommitLog(git: Git, from: String, to: String): List[CommitInfo] = {
@scala.annotation.tailrec
def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[CommitInfo]): List[CommitInfo] =
i.hasNext match {
case true => {
val revCommit = i.next
if(revCommit.name == from){
logs
} else {
getCommitLog(i, logs :+ new CommitInfo(revCommit))
}
}
case false => logs
}
val revWalk = new RevWalk(git.getRepository)
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(to)))
val commits = getCommitLog(revWalk.iterator, Nil)
revWalk.release
commits.reverse
}
// TODO swap parameters 'from' and 'to'!?
def getCommitLog(git: Git, from: String, to: String): List[CommitInfo] =
getCommitLogs(git, to)(_.getName == from)
/**
* Returns the latest RevCommit of the specified path.
@@ -348,7 +314,7 @@ object JGitUtil {
* @return the list of latest commit
*/
def getLatestCommitFromPaths(git: Git, paths: List[String], revision: String): Map[String, RevCommit] = {
val start = git.getRepository.resolve(revision)
val start = getRevCommitFromId(git, git.getRepository.resolve(revision))
paths.map { path =>
val commit = git.log.add(start).addPath(path).setMaxCount(1).call.iterator.next
(path, commit)
@@ -368,128 +334,130 @@ object JGitUtil {
if(large == false && FileUtil.isLarge(loader.getSize)){
None
} else {
val db = git.getRepository.getObjectDatabase
try {
using(git.getRepository.getObjectDatabase){ db =>
Some(db.open(id).getBytes)
} finally {
db.close
}
}
} catch {
case e: MissingObjectException => None
}
def getDiffs(git: Git, id: String, fetchContent: Boolean = true): List[DiffInfo] = {
/**
* Returns the tuple of diff of the given commit and the previous commit id.
*/
def getDiffs(git: Git, id: String, fetchContent: Boolean = true): (List[DiffInfo], Option[String]) = {
@scala.annotation.tailrec
def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[RevCommit]): List[RevCommit] =
i.hasNext match {
case true if(logs.size < 2) => getCommitLog(i, logs :+ i.next)
case _ => logs
}
val revWalk = new RevWalk(git.getRepository)
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(id)))
val commits = getCommitLog(revWalk.iterator, Nil)
revWalk.release
val revCommit = commits(0)
if(commits.length >= 2){
// not initial commit
val oldCommit = commits(1)
// get diff between specified commit and its previous commit
val reader = git.getRepository.newObjectReader
val oldTreeIter = new CanonicalTreeParser
oldTreeIter.reset(reader, git.getRepository.resolve(oldCommit.name + "^{tree}"))
val newTreeIter = new CanonicalTreeParser
newTreeIter.reset(reader, git.getRepository.resolve(id + "^{tree}"))
import scala.collection.JavaConverters._
git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff =>
if(!fetchContent || FileUtil.isImage(diff.getOldPath) || FileUtil.isImage(diff.getNewPath)){
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None)
} else {
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
JGitUtil.getContent(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(new String(_, "UTF-8")),
JGitUtil.getContent(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(new String(_, "UTF-8")))
using(new RevWalk(git.getRepository)){ revWalk =>
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(id)))
val commits = getCommitLog(revWalk.iterator, Nil)
val revCommit = commits(0)
if(commits.length >= 2){
// not initial commit
val oldCommit = commits(1)
(getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName))
} else {
// initial commit
using(new TreeWalk(git.getRepository)){ treeWalk =>
treeWalk.addTree(revCommit.getTree)
val buffer = new scala.collection.mutable.ListBuffer[DiffInfo]()
while(treeWalk.next){
buffer.append((if(!fetchContent){
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None)
} else {
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None,
JGitUtil.getContent(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray))
}))
}
(buffer.toList, None)
}
}.toList
} else {
// initial commit
val walk = new TreeWalk(git.getRepository)
walk.addTree(revCommit.getTree)
val buffer = new scala.collection.mutable.ListBuffer[DiffInfo]()
while(walk.next){
buffer.append((if(!fetchContent){
DiffInfo(ChangeType.ADD, null, walk.getPathString, None, None)
} else {
DiffInfo(ChangeType.ADD, null, walk.getPathString, None,
JGitUtil.getContent(git, walk.getObjectId(0), false).filter(FileUtil.isText).map(new String(_, "UTF-8")))
}))
}
walk.release
buffer.toList
}
}
def getDiffs(git: Git, from: String, to: String, fetchContent: Boolean): List[DiffInfo] = {
val reader = git.getRepository.newObjectReader
val oldTreeIter = new CanonicalTreeParser
oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}"))
val newTreeIter = new CanonicalTreeParser
newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}"))
import scala.collection.JavaConverters._
git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff =>
if(!fetchContent || FileUtil.isImage(diff.getOldPath) || FileUtil.isImage(diff.getNewPath)){
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None)
} else {
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
JGitUtil.getContent(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
JGitUtil.getContent(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray))
}
}.toList
}
/**
* Returns the list of branch names of the specified commit.
*/
def getBranchesOfCommit(git: Git, commitId: String): List[String] = {
val walk = new org.eclipse.jgit.revwalk.RevWalk(git.getRepository)
try {
val commit = walk.parseCommit(git.getRepository.resolve(commitId + "^0"))
git.getRepository.getAllRefs.entrySet.asScala.filter { e =>
(e.getKey.startsWith(Constants.R_HEADS) && walk.isMergedInto(commit, walk.parseCommit(e.getValue.getObjectId)))
}.map { e =>
e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_HEADS.length)
}.toList.sorted
} finally {
walk.release
def getBranchesOfCommit(git: Git, commitId: String): List[String] =
using(new RevWalk(git.getRepository)){ revWalk =>
defining(revWalk.parseCommit(git.getRepository.resolve(commitId + "^0"))){ commit =>
git.getRepository.getAllRefs.entrySet.asScala.filter { e =>
(e.getKey.startsWith(Constants.R_HEADS) && revWalk.isMergedInto(commit, revWalk.parseCommit(e.getValue.getObjectId)))
}.map { e =>
e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_HEADS.length)
}.toList.sorted
}
}
}
/**
* Returns the list of tags of the specified commit.
*/
def getTagsOfCommit(git: Git, commitId: String): List[String] = {
val walk = new org.eclipse.jgit.revwalk.RevWalk(git.getRepository)
try {
val commit = walk.parseCommit(git.getRepository.resolve(commitId + "^0"))
git.getRepository.getAllRefs.entrySet.asScala.filter { e =>
(e.getKey.startsWith(Constants.R_TAGS) && walk.isMergedInto(commit, walk.parseCommit(e.getValue.getObjectId)))
}.map { e =>
e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_TAGS.length)
}.toList.sorted.reverse
} finally {
walk.release
def getTagsOfCommit(git: Git, commitId: String): List[String] =
using(new RevWalk(git.getRepository)){ revWalk =>
defining(revWalk.parseCommit(git.getRepository.resolve(commitId + "^0"))){ commit =>
git.getRepository.getAllRefs.entrySet.asScala.filter { e =>
(e.getKey.startsWith(Constants.R_TAGS) && revWalk.isMergedInto(commit, revWalk.parseCommit(e.getValue.getObjectId)))
}.map { e =>
e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_TAGS.length)
}.toList.sorted.reverse
}
}
}
def initRepository(dir: java.io.File): Unit = {
val repository = new RepositoryBuilder().setGitDir(dir).setBare.build
try {
def initRepository(dir: java.io.File): Unit =
using(new RepositoryBuilder().setGitDir(dir).setBare.build){ repository =>
repository.create
setReceivePack(repository)
} finally {
repository.close
}
}
def cloneRepository(from: java.io.File, to: java.io.File): Unit =
using(Git.cloneRepository.setURI(from.toURI.toString).setDirectory(to).setBare(true).call){ git =>
setReceivePack(git.getRepository)
}
def isEmpty(git: Git): Boolean = git.getRepository.resolve(Constants.HEAD) == null
private def setReceivePack(repository: org.eclipse.jgit.lib.Repository): Unit = {
val config = repository.getConfig
config.setBoolean("http", null, "receivepack", true)
config.save
private def setReceivePack(repository: org.eclipse.jgit.lib.Repository): Unit =
defining(repository.getConfig){ config =>
config.setBoolean("http", null, "receivepack", true)
config.save
}
def getDefaultBranch(git: Git, repository: RepositoryService.RepositoryInfo,
revstr: String = ""): Option[(ObjectId, String)] = {
Seq(
if(revstr.isEmpty) repository.repository.defaultBranch else revstr,
repository.branchList.head
).map { rev =>
(git.getRepository.resolve(rev), rev)
}.find(_._1 != null)
}
}

View File

@@ -0,0 +1,72 @@
package util
/**
* Define key strings for request attributes, session attributes or flash attributes.
*/
object Keys {
/**
* Define session keys.
*/
object Session {
/**
* Session key for the logged in account information.
*/
val LoginAccount = "LOGIN_ACCOUNT"
/**
* Session key for the redirect URL.
*/
val Redirect = "REDIRECT"
/**
* Session key for the issue search condition in dashboard.
*/
val DashboardIssues = "dashboard/issues"
/**
* Session key for the pull request search condition in dashboard.
*/
val DashboardPulls = "dashboard/pulls"
/**
* Generate session key for the issue search condition.
*/
def Issues(owner: String, name: String) = s"${owner}/${name}/issues"
/**
* Generate session key for the pull request search condition.
*/
def Pulls(owner: String, name: String) = s"${owner}/${name}/pulls"
/**
* Generate session key for the upload filename.
*/
def Upload(fileId: String) = s"upload_${fileId}"
}
/**
* Define request keys.
*/
object Request {
/**
* Request key for the Ajax request flag.
*/
val Ajax = "AJAX"
/**
* Request key for the username which is used during Git repository access.
*/
val UserName = "USER_NAME"
/**
* Generate request key for the request cache.
*/
def Cache(key: String) = s"cache.${key}"
}
}

View File

@@ -0,0 +1,104 @@
package util
import util.ControlUtil._
import service.SystemSettingsService
import com.novell.ldap._
import service.SystemSettingsService.Ldap
import scala.annotation.tailrec
/**
* Utility for LDAP authentication.
*/
object LDAPUtil {
private val LDAP_VERSION: Int = 3
/**
* Try authentication by LDAP using given configuration.
* Returns Right(mailAddress) if authentication is successful, otherwise Left(errorMessage).
*/
def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, String] = {
bind(
ldapSettings.host,
ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
ldapSettings.bindDN.getOrElse(""),
ldapSettings.bindPassword.getOrElse("")
) match {
case Some(conn) => {
withConnection(conn) { conn =>
findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match {
case Some(userDN) => userAuthentication(ldapSettings, userDN, password)
case None => Left("User does not exist")
}
}
}
case None => Left("System LDAP authentication failed.")
}
}
private def userAuthentication(ldapSettings: Ldap, userDN: String, password: String): Either[String, String] = {
bind(
ldapSettings.host,
ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
userDN,
password
) match {
case Some(conn) => {
withConnection(conn) { conn =>
findMailAddress(conn, userDN, ldapSettings.mailAttribute) match {
case Some(mailAddress) => Right(mailAddress)
case None => Left("Can't find mail address.")
}
}
}
case None => Left("User LDAP Authentication Failed.")
}
}
private def bind(host: String, port: Int, dn: String, password: String): Option[LDAPConnection] = {
val conn: LDAPConnection = new LDAPConnection
try {
conn.connect(host, port)
conn.bind(LDAP_VERSION, dn, password.getBytes)
Some(conn)
} catch {
case e: Exception => {
if (conn.isConnected) conn.disconnect()
None
}
}
}
private def withConnection[T](conn: LDAPConnection)(f: LDAPConnection => T): T = {
try {
f(conn)
} finally {
conn.disconnect()
}
}
private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String): Option[String] = {
@tailrec
def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = {
if(results.hasMore){
getEntries(results, entries :+ (try {
Option(results.next)
} catch {
case ex: LDAPReferralException => None // NOTE(tanacasino): Referral follow is off. so ignores it.(for AD)
}))
} else {
entries.flatten
}
}
getEntries(conn.search(baseDN, LDAPConnection.SCOPE_SUB, userNameAttribute + "=" + userName, null, false)).collectFirst {
case x => x.getDN
}
}
private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] =
defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false)){ results =>
optionIf (results.hasMore) {
Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
}
}
}

View File

@@ -0,0 +1,36 @@
package util
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.locks.{ReentrantLock, Lock}
import util.ControlUtil._
object LockUtil {
/**
* lock objects
*/
private val locks = new ConcurrentHashMap[String, Lock]()
/**
* Returns the lock object for the specified repository.
*/
private def getLockObject(key: String): Lock = synchronized {
if(!locks.containsKey(key)){
locks.put(key, new ReentrantLock())
}
locks.get(key)
}
/**
* Synchronizes a given function which modifies the working copy of the wiki repository.
*/
def lock[T](key: String)(f: => T): T = defining(getLockObject(key)){ lock =>
try {
lock.lock()
f
} finally {
lock.unlock()
}
}
}

View File

@@ -0,0 +1,106 @@
package util
import scala.concurrent._
import ExecutionContext.Implicits.global
import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
import org.slf4j.LoggerFactory
import app.Context
import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService}
import servlet.Database
import SystemSettingsService.Smtp
trait Notifier extends RepositoryService with AccountService with IssuesService {
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
(msg: String => String)(implicit context: Context): Unit
protected def recipients(issue: model.Issue)(notify: String => Unit)(implicit context: Context) =
(
// individual repository's owner
issue.userName ::
// collaborators
getCollaborators(issue.userName, issue.repositoryName) :::
// participants
issue.openedUserName ::
getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName)
)
.distinct
.withFilter ( _ != context.loginAccount.get.userName ) // the operation in person is excluded
.foreach ( getAccountByUserName(_) filterNot (_.isGroupAccount) foreach (x => notify(x.mailAddress)) )
}
object Notifier {
// TODO We want to be able to switch to mock.
def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match {
case settings if settings.notification => new Mailer(settings.smtp.get)
case _ => new MockMailer
}
def msgIssue(url: String) = (content: String) => s"""
|${content}<br/>
|--<br/>
|<a href="${url}">View it on GitBucket</a>
""".stripMargin
def msgPullRequest(url: String) = (content: String) => s"""
|${content}<hr/>
|View, comment on, or merge it at:<br/>
|<a href="${url}">${url}</a>
""".stripMargin
def msgComment(url: String) = (content: String) => s"""
|${content}<br/>
|--<br/>
|<a href="${url}">View it on GitBucket</a>
""".stripMargin
def msgStatus(url: String) = (content: String) => s"""
|${content} <a href="${url}">#${url split('/') last}</a>
""".stripMargin
}
class Mailer(private val smtp: Smtp) extends Notifier {
private val logger = LoggerFactory.getLogger(classOf[Mailer])
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
(msg: String => String)(implicit context: Context) = {
val database = Database(context.request.getServletContext)
val f = future {
val email = new HtmlEmail
email.setHostName(smtp.host)
email.setSmtpPort(smtp.port.get)
smtp.user.foreach { user =>
email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse("")))
}
smtp.ssl.foreach { ssl =>
email.setSSLOnConnect(ssl)
}
email.setFrom("notifications@gitbucket.com", context.loginAccount.get.userName)
email.setHtmlMsg(msg(view.Markdown.toHtml(content, r, false, true)))
// TODO Can we use the Database Session in other than Transaction Filter?
database withSession {
getIssue(r.owner, r.name, issueId.toString) foreach { issue =>
email.setSubject(s"[${r.name}] ${issue.title} (#${issueId})")
recipients(issue) {
email.getToAddresses.clear
email.addTo(_).send
}
}
}
"Notifications Successful."
}
f onSuccess {
case s => logger.debug(s)
}
f onFailure {
case t => logger.error("Notifications Failed.", t)
}
}
}
class MockMailer extends Notifier {
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
(msg: String => String)(implicit context: Context): Unit = {}
}

View File

@@ -1,14 +1,16 @@
package util
import java.net.{URLDecoder, URLEncoder}
import org.mozilla.universalchardet.UniversalDetector
import util.ControlUtil._
object StringUtil {
def sha1(value: String): String = {
val md = java.security.MessageDigest.getInstance("SHA-1")
md.update(value.getBytes)
md.digest.map(b => "%02x".format(b)).mkString
}
def sha1(value: String): String =
defining(java.security.MessageDigest.getInstance("SHA-1")){ md =>
md.update(value.getBytes)
md.digest.map(b => "%02x".format(b)).mkString
}
def md5(value: String): String = {
val md = java.security.MessageDigest.getInstance("MD5")
@@ -25,4 +27,15 @@ object StringUtil {
def escapeHtml(value: String): String =
value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
def convertFromByteArray(content: Array[Byte]): String = new String(content, detectEncoding(content))
def detectEncoding(content: Array[Byte]): String =
defining(new UniversalDetector(null)){ detector =>
detector.handleData(content, 0, content.length)
detector.dataEnd()
detector.getDetectedCharset match {
case null => "UTF-8"
case e => e
}
}
}

View File

@@ -8,7 +8,7 @@ trait Validations {
* Constraint for the identifier such as user name, repository name or page name.
*/
def identifier: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
override def validate(name: String, value: String): Option[String] =
if(!value.matches("^[a-zA-Z0-9\\-_]+$")){
Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){
@@ -25,10 +25,7 @@ trait Validations {
*/
def date(constraints: Constraint*): SingleValueType[java.util.Date] =
new SingleValueType[java.util.Date]((pattern("\\d{4}-\\d{2}-\\d{2}") +: constraints): _*){
def convert(value: String): java.util.Date = {
val formatter = new java.text.SimpleDateFormat("yyyy-MM-dd")
formatter.parse(value)
}
def convert(value: String): java.util.Date = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(value)
}
}

View File

@@ -14,18 +14,19 @@ trait AvatarImageProvider { self: RequestCache =>
mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html = {
val src = getAccountByUserName(userName).map { account =>
if(account.image.isEmpty){
if(account.image.isEmpty && getSystemSettings().gravatar){
s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress)}?s=${size}"""
} else {
s"""${context.path}/${userName}/_avatar"""
}
} getOrElse {
if(mailAddress.nonEmpty){
if(mailAddress.nonEmpty && getSystemSettings().gravatar){
s"""http://www.gravatar.com/avatar/${StringUtil.md5(mailAddress)}?s=${size}"""
} else {
s"""${context.path}/${userName}/_avatar"""
}
}
if(tooltip){
Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title="${userName}"/>""")
} else {

View File

@@ -52,7 +52,7 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
}
class GitBucketVerbatimSerializer extends VerbatimSerializer {
def serialize(node: VerbatimNode, printer: Printer) {
def serialize(node: VerbatimNode, printer: Printer): Unit = {
printer.println.print("<pre")
if (!StringUtils.isEmpty(node.getType)) {
printer.print(" class=").print('"').print("prettyprint ").print(node.getType).print('"')
@@ -98,11 +98,11 @@ class GitBucketHtmlSerializer(
}
}
private def printAttribute(name: String, value: String) {
private def printAttribute(name: String, value: String): Unit = {
printer.print(' ').print(name).print('=').print('"').print(value).print('"')
}
override def visit(node: TextNode) {
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

View File

@@ -31,9 +31,8 @@ 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 = {
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink))
}
/**
* Returns &lt;img&gt; which displays the avatar icon.
@@ -51,9 +50,17 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
def link(value: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context): Html =
Html(convertRefsLinks(value, repository))
def cut(value: String, length: Int): String =
if(value.length > length){
value.substring(0, length) + "..."
} else {
value
}
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]+?)\\]", s"""<a href="${context.path}/$$1/$$2/tree/$$3">$$3</a>""")
.replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1/$$2/tree/$$3">$$3</a>""")
@@ -73,14 +80,12 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
/**
* Generates the url to the account page.
*/
def url(userName: String)(implicit context: app.Context): String =
s"${context.path}/${userName}"
def url(userName: String)(implicit context: app.Context): String = s"${context.path}/${userName}"
/**
* Returns the url to the root of assets.
*/
def assets(implicit context: app.Context): String =
s"${context.path}/assets"
def assets(implicit context: app.Context): String = s"${context.path}/assets"
def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime

View File

@@ -13,34 +13,37 @@
<div class="span6">
@if(account.isEmpty){
<fieldset>
<label for="userName"><strong>User name</strong></label>
<label for="userName" class="strong">User name</label>
<input type="text" name="userName" id="userName" value=""/>
<span id="error-userName" class="error"></span>
</fieldset>
}
@if(account.map(_.password.nonEmpty).getOrElse(true)){
<fieldset>
<label for="password" class="strong">
Password
@if(account.nonEmpty){
(Input to change password)
}
</label>
<input type="password" name="password" id="password" value=""/>
<span id="error-password" class="error"></span>
</fieldset>
}
<fieldset>
<label for="password"><strong>Password</strong>
@if(account.nonEmpty){
(Input to change password)
}
</label>
<input type="password" name="password" id="password" value=""/>
<span id="error-password" class="error"></span>
</fieldset>
<fieldset>
<label for="mailAddress"><strong>Mail Address</strong></label>
<label for="mailAddress" class="strong">Mail Address</label>
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
<span id="error-mailAddress" class="error"></span>
</fieldset>
<fieldset>
<label for="url"><strong>URL (Optional)</strong></label>
<label for="url" class="strong">URL (Optional)</label>
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
<span id="error-url" class="error"></span>
</fieldset>
</div>
<div class="span6">
<fieldset>
<label for="avatar"><strong>Image (Optional)</strong></label>
<label for="avatar" class="strong">Image (Optional)</label>
@helper.html.uploadavatar(account)
</fieldset>
</div>

View File

@@ -8,13 +8,14 @@
@repositories.map { repository =>
<div class="block">
<div class="block-header">
<a href="@url(repository.owner)">@repository.owner</a>
/
<a href="@url(repository)">@repository.name</a>
@if(repository.repository.isPrivate){
<i class="icon-lock"></i>
}
</div>
@if(repository.repository.originUserName.isDefined){
<div class="small muted">forked from <a href="@path/@repository.repository.parentUserName/@repository.repository.parentRepositoryName">@repository.repository.parentUserName/@repository.repository.parentRepositoryName</a></div>
}
@if(repository.repository.description.isDefined){
<div>@repository.repository.description</div>
}

View File

@@ -8,17 +8,139 @@
<div class="box">
<div class="box-header">System Settings</div>
<div class="box-content">
<label><strong>Account registration</strong></label>
<!--====================================================================-->
<!-- Account registration -->
<!--====================================================================-->
<label class="strong">Account registration</label>
<fieldset>
<label>
<input type="radio" name="allowAccountRegistration" value="true"@if(settings.allowAccountRegistration){ checked}>
<strong>Allow</strong> - Users can create account by themselves.
<span class="strong">Allow</span> - Users can create account by themselves.
</label>
<label>
<input type="radio" name="allowAccountRegistration" value="false"@if(!settings.allowAccountRegistration){ checked}>
<strong>Deny</strong> - Only administrators can create account.
<span class="strong">Deny</span> - Only administrators can create account.
</label>
</fieldset>
<!--====================================================================-->
<!-- Services -->
<!--====================================================================-->
<hr>
<label class="strong">Services</label>
<fieldset>
<label>
<input type="checkbox" name="gravatar"@if(settings.gravatar){ checked}/>
Gravatar
</label>
</fieldset>
<!--====================================================================-->
<!-- Authentication -->
<!--====================================================================-->
<hr>
<label class="strong">Authentication</label>
<fieldset>
<label>
<input type="checkbox" id="ldapAuthentication" name="ldapAuthentication"@if(settings.ldap){ checked}/>
LDAP
</label>
</fieldset>
<div class="form-horizontal ldap">
<div class="control-group">
<label class="control-label" for="ldapHost">LDAP Host</label>
<div class="controls">
<input type="text" id="ldapHost" name="ldap.host" value="@settings.ldap.map(_.host)"/>
<span id="error-ldap_host" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapPort">LDAP Port</label>
<div class="controls">
<input type="text" id="ldapPort" name="ldap.port" class="input-mini" value="@settings.ldap.map(_.port)"/>
<span id="error-ldap_port" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapBindDN">Bind DN</label>
<div class="controls">
<input type="text" id="ldapBindDN" name="ldap.bindDN" value="@settings.ldap.map(_.bindDN)"/>
<span id="error-ldap_bindDN" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapBindPassword">Bind Password</label>
<div class="controls">
<input type="password" id="ldapBindPassword" name="ldap.bindPassword" value="@settings.ldap.map(_.bindPassword)"/>
<span id="error-ldap_bindPassword" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapBaseDN">Base DN</label>
<div class="controls">
<input type="text" id="ldapBaseDN" name="ldap.baseDN" value="@settings.ldap.map(_.baseDN)"/>
<span id="error-ldap_baseDN" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapUserNameAttribute">User name attribute</label>
<div class="controls">
<input type="text" id="ldapUserNameAttribute" name="ldap.userNameAttribute" value="@settings.ldap.map(_.userNameAttribute)"/>
<span id="error-ldap_userNameAttribute" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapMailAttribute">Mail address attribute</label>
<div class="controls">
<input type="text" id="ldapMailAttribute" name="ldap.mailAttribute" value="@settings.ldap.map(_.mailAttribute)"/>
<span id="error-ldap_mailAttribute" class="error"></span>
</div>
</div>
</div>
<!--====================================================================-->
<!-- Notification email -->
<!--====================================================================-->
<hr>
<label class="strong">Notification email</label>
<fieldset>
<label>
<input type="checkbox" id="notification" name="notification"@if(settings.notification){ checked}/>
Send notifications
</label>
</fieldset>
<div class="form-horizontal notification">
<div class="control-group">
<label class="control-label" for="smtpHost">SMTP Host</label>
<div class="controls">
<input type="text" id="smtpHost" name="smtp.host" value="@settings.smtp.map(_.host)"/>
<span id="error-smtp_host" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="smtpPort">SMTP Port</label>
<div class="controls">
<input type="text" id="smtpPort" name="smtp.port" class="input-mini" value="@settings.smtp.map(_.port)"/>
<span id="error-smtp_port" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="smtpUser">SMTP User</label>
<div class="controls">
<input type="text" id="smtpUser" name="smtp.user" value="@settings.smtp.map(_.user)"/>
</div>
</div>
<div class="control-group">
<label class="control-label" for="smtpPassword">SMTP Password</label>
<div class="controls">
<input type="password" id="smtpPassword" name="smtp.password" value="@settings.smtp.map(_.password)"/>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" name="smtp.ssl"@if(settings.smtp.flatMap(_.ssl).getOrElse(false)){ checked}/> Enable SSL
</label>
</div>
</div>
</div>
</div>
</div>
<fieldset>
@@ -26,4 +148,15 @@
</fieldset>
</form>
}
}
}
<script>
$(function(){
$('#notification').change(function(){
$('.notification input').prop('disabled', !$(this).prop('checked'));
}).change();
$('#ldapAuthentication').change(function(){
$('.ldap input').prop('disabled', !$(this).prop('checked'));
}).change();
});
</script>

View File

@@ -7,23 +7,23 @@
<div class="row-fluid">
<div class="span7">
<fieldset>
<label for="groupName"><strong>Group name</strong></label>
<label for="groupName" class="strong">Group name</label>
<span id="error-groupName" class="error"></span>
<input type="text" name="groupName" id="groupName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
</fieldset>
<fieldset>
<label><strong>URL (Optional)</strong></label>
<label class="strong">URL (Optional)</label>
<span id="error-url" class="error"></span>
<input type="text" name="url" id="url" style="width: 300px;" value="@account.map(_.url)"/>
</fieldset>
<fieldset>
<label for="avatar"><strong>Image (Optional)</strong></label>
<label for="avatar" class="strong">Image (Optional)</label>
@helper.html.uploadavatar(account)
</fieldset>
</div>
<div class="span5">
<fieldset>
<label><strong>Members</strong></label>
<label class="strong">Members</label>
<ul id="members" class="collaborator">
@members.map { userName =>
<li data-name="@userName">
@@ -32,7 +32,7 @@
</li>
}
</ul>
<input type="text" id="memberName" style="width: 200px; margin-bottom: 0px;"/>
@helper.html.account("memberName", 200)
<input type="button" class="btn" value="Add" id="addMember"/>
<input type="hidden" id="memberNames" name="memberNames" value="@members.mkString(",")"/>
<div>
@@ -50,15 +50,6 @@
}
<script>
$(function(){
$('#memberName').typeahead({
source: function (query, process) {
return $.get('@path/_user/proposals', { query: query },
function (data) {
return process(data.options);
});
}
});
$('#addMember').click(function(){
$('#error-memberName').text('');
var userName = $('#memberName').val();

View File

@@ -6,27 +6,29 @@
<div class="row-fluid">
<div class="span6">
<fieldset>
<label for="userName"><strong>Username</strong></label>
<label for="userName" class="strong">Username</label>
<span id="error-userName" class="error"></span>
<input type="text" name="userName" id="userName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
</fieldset>
@if(account.map(_.password.nonEmpty).getOrElse(true)){
<fieldset>
<label for="password" class="strong">
Password
@if(account.isDefined){
(Input to change password)
}
</label>
<span id="error-password" class="error"></span>
<input type="password" name="password" id="password" value="" autocomplete="off"/>
</fieldset>
}
<fieldset>
<label for="password">
<strong>Password</strong>
@if(account.isDefined){
(Input to change password)
}
</label>
<span id="error-password" class="error"></span>
<input type="password" name="password" id="password" value="" autocomplete="off"/>
</fieldset>
<fieldset>
<label for="mailAddress"><strong>Mail Address</strong></label>
<label for="mailAddress" class="strong">Mail Address</label>
<span id="error-mailAddress" class="error"></span>
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
</fieldset>
<fieldset>
<label><strong>User Type</strong></label>
<label class="strong">User Type</label>
<label for="userType_Normal">
<input type="radio" name="isAdmin" id="userType_Normal" value="false"@if(account.isEmpty || !account.get.isAdmin){ checked}/> Normal
</label>
@@ -35,14 +37,14 @@
</label>
</fieldset>
<fieldset>
<label><strong>URL (Optional)</strong></label>
<label class="strong">URL (Optional)</label>
<span id="error-url" class="error"></span>
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
</fieldset>
</div>
<div class="span6">
<fieldset>
<label for="avatar"><strong>Image (Optional)</strong></label>
<label for="avatar" class="strong">Image (Optional)</label>
@helper.html.uploadavatar(account)
</fieldset>
</div>

View File

@@ -0,0 +1,40 @@
@(listparts: twirl.api.Html,
counts: List[service.PullRequestService.PullRequestCount],
repositories: List[(String, String, Int)],
condition: service.IssuesService.IssueSearchCondition,
filter: String)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Your Issues"){
@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>
}

View File

@@ -3,6 +3,7 @@
<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>
}
</ul>

View File

@@ -1,11 +1,24 @@
@(active: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
<div class="pull-right">
<div class="input-prepend">
<a href="@path/@repository.owner/@repository.name/fork" class="btn" style="margin-bottom: 10px;">Fork</a>
<span class="add-on"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
</div>
</div>
<div class="head">
<a href="@url(repository.owner)">@repository.owner</a> / <a href="@url(repository)">@repository.name</a>
<a href="@url(repository.owner)">@repository.owner</a> / <a href="@url(repository)" class="strong">@repository.name</a>
@if(repository.repository.isPrivate){
<i class="icon-lock"></i>
}
@defining(repository.repository){ x =>
@if(repository.repository.originRepositoryName.isDefined){
<div class="forked">
forked from <a href="@path/@x.parentUserName/@x.parentRepositoryName">@x.parentUserName/@x.parentRepositoryName</a>
</div>
}
}
</div>
<table class="global-nav box-header">
<tr>
@@ -14,16 +27,28 @@
</th>
<th class="box-header@if(active=="issues"){ active}">
<a href="@url(repository)/issues">Issues</a>
@if(repository.issueCount > 0){
<span class="badge">@repository.issueCount</span>
}
</th>
<th class="box-header@if(active=="pulls"){ active}">
<a href="@url(repository)/pulls">Pull Requests</a>
@if(repository.pullCount > 0){
<span class="badge">@repository.pullCount</span>
}
</th>
<th class="box-header@if(active=="wiki"){ active}">
<a href="@url(repository)/wiki">Wiki</a>
</th>
<th class="box-header@if(active=="network"){ active}">
<a href="@url(repository)/network/members">Network</a>
</th>
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || loginAccount.get.userName == repository.owner)){
<th class="box-header@if(active=="settings"){ active}">
<a href="@url(repository)/settings">Settings</a>
</th>
}
</tr>
</tr>
</table>
<script type="text/javascript">
$(function(){

View File

@@ -0,0 +1,15 @@
@(id: String, width: Int)(implicit context: app.Context)
@import context._
<input type="text" name="@id" id="@id" style="width: @{width}px; margin-bottom: 0px;"/>
<script>
$(function(){
$('#@id').typeahead({
source: function (query, process) {
return $.get('@path/_user/proposals', { query: query },
function (data) {
return process(data.options);
});
}
});
});
</script>

View File

@@ -1,45 +1,96 @@
@(activities: List[model.Activity])(implicit context: app.Context)
@import context._
@import view.helpers._
@if(activities.isEmpty){
No activity
} else {
@activities.map { activity =>
<div class="block">
<div class="muted small">@datetime(activity.activityDate)</div>
<div class="strong">
@avatar(activity.activityUserName, 16)
@activityMessage(activity.message)
</div>
@activity.additionalInfo.map { additionalInfo =>
@if(additionalInfo.nonEmpty){
@(activity.activityType match {
case "create_wiki" => {
<div class="small activity-message">Created <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${additionalInfo}"}>{additionalInfo}</a>.</div>
}
case "edit_wiki" => {
<div class="small activity-message">Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${additionalInfo}"}>{additionalInfo}</a>.</div>
}
case "push" => {
<div class="small activity-message">
{additionalInfo.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) =>
if(i == 3){
<div>...</div>
} else {
<div>
<a href={s"${path}/${activity.userName}/${activity.repositoryName}/commit/${commit.substring(0, 40)}"} class="monospace">{commit.substring(0, 7)}</a>
<span>{commit.substring(41)}</span>
</div>
}
}}
</div>
}
case _ => {
<div class=" activity-message">{additionalInfo}</div>
}
})
@(activity.activityType match {
case "open_issue" => detailActivity(activity, "activity-issue.png")
case "comment_issue" => 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")
case "merge_pullreq" => detailActivity(activity, "activity-merge.png")
case "create_repository" => simpleActivity(activity, "activity-create-repository.png")
case "create_branch" => simpleActivity(activity, "activity-branch.png")
case "create_tag" => simpleActivity(activity, "activity-tag.png")
case "fork" => simpleActivity(activity, "activity-fork.png")
case "push" => customActivity(activity, "activity-commit.png"){
<div class="small activity-message">
{activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) =>
if(i == 3){
<div>...</div>
} else {
if(commit.nonEmpty){
<div>
<a href={s"${path}/${activity.userName}/${activity.repositoryName}/commit/${commit. substring(0, 40)}"} class="monospace">{commit.substring(0, 7)}</a>
<span>{commit.substring(41)}</span>
</div>
}
}
}}
</div>
}
}
case "create_wiki" => customActivity(activity, "activity-wiki.png"){
<div class="small activity-message">
Created <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${activity.additionalInfo.get}"}>{activity.additionalInfo.get}</a>.
</div>
}
case "edit_wiki" => customActivity(activity, "activity-wiki.png"){
activity.additionalInfo.get.split(":") match {
case Array(pageName, commitId) =>
<div class="small activity-message">
Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}"}>{pageName}</a>.
<a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}/_compare/${commitId.substring(0, 7)}^...${commitId.substring(0, 7)}"}>View the diff »</a>
</div>
case Array(pageName) =>
<div class="small activity-message">
Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}"}>{pageName}</a>.
</div>
}
}
})
</div>
}
}
@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="strong">
@avatar(activity.activityUserName, 16)
@activityMessage(activity.message)
</div>
@activity.additionalInfo.map { additionalInfo =>
<div class=" activity-message">@additionalInfo</div>
}
</div>
}
@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="strong">
@avatar(activity.activityUserName, 16)
@activityMessage(activity.message)
</div>
@additionalInfo
</div>
}
@simpleActivity(activity: model.Activity, image: String) = {
<div class="activity-icon-small"><img src="@assets/common/images/@image"/></div>
<div class="activity-content">
<div>
@avatar(activity.activityUserName, 16)
@activityMessage(activity.message)
<span class="muted small">@datetime(activity.activityDate)</span>
</div>
</div>
}

View File

@@ -0,0 +1,36 @@
@(id: String, value: String)(html: Html)
<div class="input-append">
@html
<span id="@id" class="add-on btn" data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="icon-check"></i></span>
</div>
<script>
// copy to clipboard
(function() {
// Find ZeroClipboard.swf file URI from ZeroClipboard JavaScript file path.
// NOTE(tanacasino) I think this way is wrong... but i don't know correct way.
var moviePath = (function() {
var zclipjs = "ZeroClipboard.min.js";
var scripts = document.getElementsByTagName("script");
var i = scripts.length;
while(i--) {
var match = scripts[i].src.match(zclipjs + "$");
if(match) {
return match.input.substr(0, match.input.length - 6) + 'swf';
}
}
})();
var clip = new ZeroClipboard($("#@id"), {
moviePath: moviePath
});
var title = $('#@id').attr('title');
$('#@id').removeAttr('title')
clip.on('complete', function(client, args) {
$(clip.htmlBridge).attr('title', 'copied!').tooltip('fixTitle').tooltip('show');
$(clip.htmlBridge).attr('title', title).tooltip('fixTitle');
});
$(clip.htmlBridge).tooltip({
title: title,
placement: $('#@id').attr('data-placement')
});
})();
</script>

View File

@@ -1,7 +1,39 @@
@(diffs: Seq[util.JGitUtil.DiffInfo], repository: service.RepositoryService.RepositoryInfo, commitId: Option[String])(implicit context: app.Context)
@(diffs: Seq[util.JGitUtil.DiffInfo],
repository: service.RepositoryService.RepositoryInfo,
newCommitId: Option[String],
oldCommitId: Option[String],
showIndex: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
@if(showIndex){
<div>
<div class="pull-right" style="margin-bottom: 10px;">
<input id="toggle-file-list" type="button" class="btn" value="Show file list"/>
</div>
Showing @diffs.size changed @plural(diffs.size, "file")
</div>
<ul id="commit-file-list" style="display: none;">
@diffs.zipWithIndex.map { case (diff, i) =>
<li@if(i > 0){ class="border"}>
<a href="#diff-@i">
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
<img src="@assets/common/images/diff_move.png"/> @diff.oldPath -> @diff.newPath
}
@if(diff.changeType == ChangeType.ADD){
<img src="@assets/common/images/diff_add.png"/> @diff.newPath
}
@if(diff.changeType == ChangeType.MODIFY){
<img src="@assets/common/images/diff_edit.png"/> @diff.newPath
}
@if(diff.changeType == ChangeType.DELETE){
<img src="@assets/common/images/diff_delete.png"/> @diff.oldPath
}
</a>
</li>
}
</ul>
}
@diffs.zipWithIndex.map { case (diff, i) =>
<a name="diff-@i"></a>
<table class="table table-bordered">
@@ -9,17 +41,27 @@
<th style="font-weight: normal;" class="box-header">
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
@diff.oldPath -> @diff.newPath
@if(newCommitId.isDefined){
<div class="pull-right align-right">
<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(newCommitId.isDefined){
<div class="pull-right align-right">
<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
}
@if(commitId.isDefined){
<div class="pull-right align-right">
<a href="@url(repository)/blob/@commitId.get/@diff.newPath" class="btn btn-small">View file @@ @commitId.get.substring(0, 10)</a>
</div>
@if(oldCommitId.isDefined){
<div class="pull-right align-right">
<a href="@url(repository)/blob/@oldCommitId.get/@diff.oldPath" class="btn btn-small">View file @@ @oldCommitId.get.substring(0, 10)</a>
</div>
}
}
</th>
</tr>
@@ -90,6 +132,17 @@ function diffUsingJS(oldTextId, newTextId, outputId) {
}
$(function(){
@if(showIndex){
$('#toggle-file-list').click(function(){
$('#commit-file-list').toggle();
if($(this).val() == 'Show file list'){
$(this).val('Hide file list');
} else {
$(this).val('Show file list');
}
});
}
@diffs.zipWithIndex.map { case (diff, i) =>
@if(diff.newContent != None || diff.oldContent != None){
if($('#oldText-@i').length > 0){

View File

@@ -1,10 +1,13 @@
@(buttonValue: String = "")(body: Html)
<div class="btn-group">
<button class="btn btn-mini dropdown-toggle" data-toggle="dropdown">
@if(buttonValue == ""){
@(value: String = "", prefix: String = "", mini: Boolean = true, style: String = "")(body: Html)
<div class="btn-group"@if(style.nonEmpty){ style="@style"}>
<button class="btn dropdown-toggle@if(mini){ btn-mini}" data-toggle="dropdown">
@if(value.isEmpty){
<i class="icon-cog"></i>
} else {
<strong>@buttonValue</strong>
@if(prefix.nonEmpty){
<span class="muted">@prefix:</span>
}
<span class="strong">@value</span>
}
<span class="caret"></span>
</button>

View File

@@ -32,9 +32,9 @@
<tr>
<td>
@if(repository.owner == loginAccount.get.userName){
<a href="@url(repository)"><strong>@repository.name</strong></a>
<a href="@url(repository)"><span class="strong">@repository.name</span></a>
} else {
<a href="@url(repository)">@repository.owner/<strong>@repository.name</strong></a>
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
}
</td>
</tr>
@@ -57,7 +57,7 @@
@recentRepositories.map { repository =>
<tr>
<td>
<a href="@url(repository)">@repository.owner/<strong>@repository.name</strong></a>
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
</td>
</tr>
}

View File

@@ -0,0 +1,29 @@
@(issue: model.Issue,
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@if(loginAccount.isDefined){
<form method="POST" validate="true">
<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: 680px; height: 100px;")
</div>
</div>
<div class="pull-right">
<input type="hidden" name="issueId" value="@issue.issueId"/>
<input type="submit" class="btn btn-success" formaction="@url(repository)/issue_comments/new" value="Comment"/>
@if((!issue.isPullRequest || !issue.closed) && (hasWritePermission || issue.openedUserName == loginAccount.get.userName)){
<input type="submit" class="btn" formaction="@url(repository)/issue_comments/state" value="@{if(issue.closed) "Reopen" else "Close"}" id="action"/>
}
</div>
</form>
}
<script>
$(function(){
$('#action').click(function(){
$('<input type="hidden">').attr('name', 'action').val($(this).val().toLowerCase()).appendTo('form');
});
});
</script>

View File

@@ -0,0 +1,73 @@
@(issue: model.Issue,
comments: List[model.IssueComment],
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo,
pullreq: Option[model.PullRequest] = None)(implicit context: app.Context)
@import context._
@import view.helpers._
@comments.map { comment =>
@if(comment.action != "close" && comment.action != "reopen"){
<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">
<i class="icon-comment"></i>
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> commented
<span class="pull-right">
@datetime(comment.registeredDate)
@if(comment.action != "commit" && comment.action != "merge" && (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>
}
</span>
</div>
<div class="box-content"class="issue-content" id="commentContent-@comment.commentId">
@markdown(comment.content, repository, false, true)
</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)
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> merged commit <code>@pullreq.map(_.commitIdTo.substring(0, 7))</code>
@if(pullreq.get.requestUserName == repository.owner){
<span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> to <span class="label label-info monospace">@pullreq.map(_.branch)</span>
} else {
<span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span> to <span class="label label-info monospace">@pullreq.map(_.userName):@pullreq.map(_.branch)</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){
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> closed the pull request @datetime(comment.registeredDate)
} else {
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> 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)
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> reopened the issue @datetime(comment.registeredDate)
</div>
}
}
<script>
$(function(){
$('i.icon-pencil').click(function(){
var id = $(this).closest('a').data('comment-id');
$.get('@url(repository)/issue_comments/_data/' + id,
{
dataType : 'html'
},
function(data){
$('#commentContent-' + id).empty().html(data);
});
return false;
});
});
</script>

View File

@@ -65,7 +65,7 @@
</div>
<div class="span3">
@if(hasWritePermission){
<strong>Add Labels</strong>
<span class="strong">Add Labels</span>
<div>
<div id="label-list">
<ul class="label-list nav nav-pills nav-stacked">
@@ -111,7 +111,7 @@ $(function(){
if(milestoneId == ''){
$('#label-milestone').text('No milestone');
} else {
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<strong>').text(title)));
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<span class="strong">').text(title)));
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
}
$('input[name=milestoneId]').val(milestoneId);

View File

@@ -17,132 +17,9 @@
</ul>
<div class="row-fluid">
<div class="span10">
<div class="issue-avatar-image">@avatar(issue.openedUserName, 48)</div>
<div class="box issue-box">
<div class="box-content" style="padding: 0px;">
<div class="issue-header">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<span class="pull-right"><a class="btn btn-small" href="#" id="edit">Edit</a></span>
}
<div class="small muted">
<a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a> opened this issue @datetime(issue.registeredDate)
</div>
<h4 id="issueTitle">@issue.title</h4>
</div>
<div class="issue-info">
<span id="label-assigned">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20) <a href="@url(userName)" class="username strong">@userName</a> is assigned
}.getOrElse("No one is assigned")
</span>
@if(hasWritePermission){
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
@collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li>
}
}
}
<div class="pull-right">
<span id="label-milestone">
@issue.milestoneId.map { milestoneId =>
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
Milestone: <strong>@milestone.title</strong>
}
}.getOrElse("No milestone")
</span>
<div id="milestone-progress-area">
@issue.milestoneId.map { milestoneId =>
@milestones.collect { case (milestone, openCount, closeCount) if(milestone.milestoneId == milestoneId) =>
@issues.milestones.html.progress(openCount + closeCount, closeCount, false)
}
}
</div>
@if(hasWritePermission){
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li>
@milestones.map { case (milestone, _, _) =>
<li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
<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_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
}
</div>
</div>
<div class="issue-content" id="issueContent">
@markdown(issue.content getOrElse "No description given.", repository, false, true)
</div>
</div>
</div>
<div class="issue-participants">
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
<strong>@participants.size</strong> @plural(participants.size, "participant")
@participants.map { participant => <a href="@url(participant)">@avatar(participant, 20, tooltip = true)</a> }
}
</div>
@comments.map { comment =>
@if(comment.action != "close" && comment.action != "reopen"){
<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">
<i class="icon-comment"></i>
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> commented
<span class="pull-right">
@datetime(comment.registeredDate)
@if(comment.action != "commit" && (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>
}
</span>
</div>
<div class="box-content"class="issue-content" id="commentContent-@comment.commentId">
@markdown(comment.content, repository, false, true)
</div>
</div>
}
@if(comment.action == "close" || comment.action == "close_comment"){
<div class="small issue-comment-action">
<span class="label label-important">Closed</span>
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> 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>
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> reopened the issue @datetime(comment.registeredDate)
</div>
}
}
@if(loginAccount.isDefined){
<form method="POST" validate="true">
<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: 680px; height: 100px;")
</div>
</div>
<div class="pull-right">
<input type="hidden" name="issueId" value="@issue.issueId"/>
<input type="submit" class="btn btn-success" formaction="@url(repository)/issue_comments/new" value="Comment"/>
@if(hasWritePermission || issue.openedUserName == loginAccount.get.userName){
<input type="submit" class="btn" formaction="@url(repository)/issue_comments/state" value="@{if(issue.closed) "Reopen" else "Close"}" id="action"/>
}
</div>
</form>
}
@issuedetail(issue, comments, collaborators, milestones, hasWritePermission, repository)
@commentlist(issue, comments, hasWritePermission, repository)
@commentform(issue, hasWritePermission, repository)
</div>
<div class="span2">
@if(issue.closed) {
@@ -152,19 +29,19 @@
}
<div class="small" style="text-align: center;">
@defining(comments.filter( _.action.contains("comment") ).size){ count =>
<strong>@count</strong> @plural(count, "comment")
<span class="strong">@count</span> @plural(count, "comment")
}
</div>
<hr/>
<div style="margin-bottom: 8px;">
<strong>Labels</strong>
<span class="strong">Labels</span>
@if(hasWritePermission){
<div class="pull-right">
@helper.html.dropdown() {
@labels.map { label =>
<li>
<a href="#" class="toggle-label" data-label-id="@label.labelId">
<i class="@{if(issueLabels.exists(_.labelId == label.labelId)) "icon-ok" else "icon-white"}"></i>
@helper.html.checkicon(issueLabels.exists(_.labelId == label.labelId))
<span class="label" style="background-color: #@label.color;">&nbsp;</span>
@label.labelName
</a>
@@ -182,82 +59,6 @@
}
<script>
$(function(){
@if(issue.assignedUserName.isDefined){
$('a.assign[data-name=@issue.assignedUserName] i').attr('class', 'icon-ok');
}
@if(issue.milestoneId.isDefined){
$('a.milestone[data-id=@issue.milestoneId] i').attr('class', 'icon-ok');
}
$('#edit').click(function(){
$.get('@url(repository)/issues/_data/@issue.issueId',
{
dataType : 'html'
},
function(data){
$('#issueContent').empty().html(data);
});
return false;
});
$('a.assign').click(function(){
var $this = $(this);
var userName = $this.data('name');
$.post('@url(repository)/issues/@issue.issueId/assign',
{
assignedUserName: userName
},
function(){
$('a.assign i.icon-ok').attr('class', 'icon-white');
if(userName == ''){
$('#label-assigned').text('No one is assigned');
} else {
$('#label-assigned').empty()
.append($this.find('img.avatar').clone(false)).append(' ')
.append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName))
.append(' is assigned');
$('a.assign[data-name=' + userName + '] i').attr('class', 'icon-ok');
}
});
});
$('a.milestone').click(function(){
var title = $(this).data('title');
var milestoneId = $(this).data('id');
$.post('@url(repository)/issues/@issue.issueId/milestone',
{
milestoneId: milestoneId
},
function(data){
console.log(data);
$('a.milestone i.icon-ok').attr('class', 'icon-white');
if(milestoneId == ''){
$('#label-milestone').text('No milestone');
$('#milestone-progress-area').empty();
} else {
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<strong>').text(title)));
$('#milestone-progress-area').html(data);
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
}
});
});
$('i.icon-pencil').click(function(){
var id = $(this).closest('a').data('comment-id');
$.get('@url(repository)/issue_comments/_data/' + id,
{
dataType : 'html'
},
function(data){
$('#commentContent-' + id).empty().html(data);
});
return false;
});
$('#action').click(function(){
$('<input type="hidden">').attr('name', 'action').val($(this).val().toLowerCase()).appendTo('form');
});
$('a.toggle-label').click(function(){
var path, icon;
var i = $(this).children('i');

View File

@@ -0,0 +1,145 @@
@(issue: model.Issue,
comments: List[model.IssueComment],
collaborators: List[String],
milestones: List[(model.Milestone, Int, Int)],
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
<div class="issue-avatar-image">@avatar(issue.openedUserName, 48)</div>
<div class="box issue-box">
<div class="box-content" style="padding: 0px;">
<div class="issue-header">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<span class="pull-right"><a class="btn btn-small" href="#" id="edit">Edit</a></span>
}
<div class="small muted">
<a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a> opened this issue @datetime(issue.registeredDate)
</div>
<h4 id="issueTitle">@issue.title</h4>
</div>
<div class="issue-info">
<span id="label-assigned">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20) <a href="@url(userName)" class="username strong">@userName</a> is assigned
}.getOrElse("No one is assigned")
</span>
@if(hasWritePermission){
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
@collaborators.map { collaborator =>
<li>
<a href="javascript:void(0);" class="assign" data-name="@collaborator">
@helper.html.checkicon(Some(collaborator) == issue.assignedUserName)@avatar(collaborator, 20) @collaborator
</a>
</li>
}
}
}
<div class="pull-right">
<span id="label-milestone">
@issue.milestoneId.map { milestoneId =>
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
Milestone: <span class="strong">@milestone.title</span>
}
}.getOrElse("No milestone")
</span>
<div id="milestone-progress-area">
@issue.milestoneId.map { milestoneId =>
@milestones.collect { case (milestone, openCount, closeCount) if(milestone.milestoneId == milestoneId) =>
@issues.milestones.html.progress(openCount + closeCount, closeCount, false)
}
}
</div>
@if(hasWritePermission){
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li>
@milestones.map { case (milestone, _, _) =>
<li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
@helper.html.checkicon(Some(milestone.milestoneId) == issue.milestoneId) @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
}
</div>
</div>
<div class="issue-content" id="issueContent">
@markdown(issue.content getOrElse "No description given.", repository, false, true)
</div>
</div>
</div>
<div class="issue-participants">
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
<span class="strong">@participants.size</span> @plural(participants.size, "participant")
@participants.map { participant => <a href="@url(participant)">@avatar(participant, 20, tooltip = true)</a> }
}
</div>
<script>
$(function(){
$('#edit').click(function(){
$.get('@url(repository)/issues/_data/@issue.issueId',
{
dataType : 'html'
},
function(data){
$('#issueContent').empty().html(data);
});
return false;
});
$('a.assign').click(function(){
var $this = $(this);
var userName = $this.data('name');
$.post('@url(repository)/issues/@issue.issueId/assign',
{
assignedUserName: userName
},
function(){
$('a.assign i.icon-ok').attr('class', 'icon-white');
if(userName == ''){
$('#label-assigned').text('No one is assigned');
} else {
$('#label-assigned').empty()
.append($this.find('img.avatar').clone(false)).append(' ')
.append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName))
.append(' is assigned');
$('a.assign[data-name=' + userName + '] i').attr('class', 'icon-ok');
}
});
});
$('a.milestone').click(function(){
var title = $(this).data('title');
var milestoneId = $(this).data('id');
$.post('@url(repository)/issues/@issue.issueId/milestone',
{
milestoneId: milestoneId
},
function(data){
console.log(data);
$('a.milestone i.icon-ok').attr('class', 'icon-white');
if(milestoneId == ''){
$('#label-milestone').text('No milestone');
$('#milestone-progress-area').empty();
} else {
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<span class="strong">').text(title)));
$('#milestone-progress-area').html(data);
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
}
});
});
});
</script>

View File

@@ -106,7 +106,7 @@
}
}
<hr/>
<strong>Labels</strong>
<span class="strong">Labels</span>
<div>
<div id="label-list">
<ul class="label-list nav nav-pills nav-stacked">
@@ -127,7 +127,7 @@
<hr/>
<input type="button" class="btn btn-block" id="manageLabel" data-toggle="button" value="Manage Labels"/>
<br/>
<strong>New label</strong>
<span class="strong">New label</span>
@_root_.issues.labels.html.edit(None, repository)
}
</div>

View File

@@ -29,52 +29,49 @@
<a class="btn@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a>
<a class="btn@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a>
</div>
<div class="btn-group">
<button class="btn dropdown-toggle" data-toggle="dropdown">
Sort:
<strong>
@if(condition.sort == "created" && condition.direction == "desc"){ Newest }
@if(condition.sort == "created" && condition.direction == "asc" ){ Oldest }
@if(condition.sort == "comments" && condition.direction == "desc"){ Most commented }
@if(condition.sort == "comments" && condition.direction == "asc" ){ Least commented }
@if(condition.sort == "updated" && condition.direction == "desc"){ Recently updated }
@if(condition.sort == "updated" && condition.direction == "asc" ){ Least recently updated }
</strong>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<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>
</ul>
</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>
@@ -94,7 +91,7 @@
<tr>
<td style="background-color: #eee;">
<div class="btn-group">
<button class="btn btn-mini" id="state"><strong>@{if(condition.state == "open") "Close" else "Reopen"}</strong></button>
<button class="btn btn-mini strong" id="state">@{if(condition.state == "open") "Close" else "Reopen"}</button>
</div>
@helper.html.dropdown("Label") {
@labels.map { label =>
@@ -145,12 +142,21 @@
<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="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 =>
@@ -158,13 +164,15 @@
}
#@issue.issueId
</span>
<div class="small muted">
<div class="small muted" style="margin-left: 20px;">
Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a> @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>
}

View File

@@ -11,12 +11,12 @@
<span id="error-title" class="error"></span>
</fieldset>
<fieldset>
<label for="description"><strong>Description</strong></label>
<label for="description" class="strong">Description</label>
<textarea id="description" name="description" style="width: 500px; height: 150px;">@milestone.map(_.description)</textarea>
<span id="error-description" class="error"></span>
</fieldset>
<fieldset>
<label for="dueDate"><strong>Due Date</strong></label>
<label for="dueDate" class="strong">Due Date</label>
@helper.html.datepicker("dueDate", milestone.flatMap(_.dueDate))
<span id="error-dueDate" class="error"></span>
</fieldset>

View File

@@ -5,7 +5,7 @@
@import context._
@import view.helpers._
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){
@html.header("milestones", repository)
@html.header("issues", repository)
@issues.html.tab("milestones", repository)
<div class="row-fluid">
<div class="span3">

View File

@@ -6,6 +6,7 @@
<head>
<meta charset="utf-8">
<title>@title</title>
<link rel="icon" href="@assets/common/images/favicon.png" type="image/vnd.microsoft.icon" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Le styles -->
<link href="@assets/bootstrap/css/bootstrap.css" rel="stylesheet">
@@ -38,7 +39,12 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="brand" href="@path/">GitBucket</a>
<a class="brand" href="@path/">
<img src="@assets/common/images/gitbucket.png"/>GitBucket
@defining(servlet.AutoUpdate.getCurrentVersion){ version =>
<span class="header-version">@version.majorVersion.@version.minorVersion</span>
}
</a>
<div class="nav-collapse collapse pull-right header-menu">
@repository.map { repository =>
<input type="text" name="query" style="width: 300px; margin-bottom: 0px;" placeholder="Search this repository"/>
@@ -54,15 +60,12 @@
}
<a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a>
} else {
<a href="@path/signin?@currentUrl" class="btn btn-last">Sign in</a>
<a href="@path/signin?redirect=@redirectUrl" class="btn btn-last">Sign in</a>
}
</div><!--/.nav-collapse -->
</div>
</div>
</form>
@defining(servlet.AutoUpdate.getCurrentVersion){ version =>
<div class="gitbucket-version">version @version.majorVersion.@version.minorVersion</div>
}
</div>
<div class="container body">
@body

View File

@@ -5,10 +5,10 @@
<div style="width: 600px; margin: 10px auto;">
<form id="form" method="post" action="@path/new" validate="true">
<fieldset>
<label for="name"><strong>Repository name</strong></label>
<label for="name" class="strong">Repository name</label>
<div class="btn-group" style="margin-bottom: 10px;" id="owner-dropdown">
<button class="btn dropdown-toggle" data-toggle="dropdown">
<strong>@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</strong>
<span class="strong">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
@@ -24,13 +24,13 @@
<span id="error-name" class="error"></span>
</fieldset>
<fieldset>
<label for="description"><strong>Description</strong> (optional)</label>
<label for="description" class="strong">Description (optional)</label>
<input type="text" name="description" id="description" style="width: 95%;"/>
</fieldset>
<fieldset class="margin">
<label>
<input type="radio" name="isPrivate" value="false" checked>
<strong>Public</strong><br>
<span class="strong">Public</span><br>
<div>
<span class="note">All users and guests can read this repository.</span>
</div>
@@ -39,7 +39,7 @@
<fieldset>
<label>
<input type="radio" name="isPrivate" value="true">
<strong>Private</strong><br>
<span class="strong">Private</span><br>
<div>
<span class="note">Only collaborators can read this repository.</span>
</div>
@@ -48,7 +48,7 @@
<fieldset class="margin">
<label for="createReadme">
<input type="checkbox" name="createReadme" id="createReadme"/>
<strong>Initialize this repository with a README</strong>
<span class="strong">Initialize this repository with a README</span>
<div>
<span class="note">This will allow you to <code>git clone</code> the repository immediately.</span>
</div>
@@ -68,6 +68,6 @@ $('#owner-dropdown a').click(function(){
$('#owner-dropdown i').attr('class', 'icon-white');
$(this).find('i').attr('class', 'icon-ok');
$('#owner-dropdown strong').html($(this).find('span').html());
$('#owner-dropdown span.strong').html($(this).find('span').html());
});
</script>

View File

@@ -0,0 +1,25 @@
@(commits: Seq[Seq[util.JGitUtil.CommitInfo]],
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
<div class="box">
<table class="table table-file-list" style="border: 1px solid silver;">
@commits.map { day =>
<tr>
<th colspan="3" class="box-header" style="font-weight: normal;">@date(day.head.time)</th>
</tr>
@day.map { commit =>
<tr>
<td style="width: 20%;">
@avatar(commit.committer, 20)
<a href="@url(commit.committer)" class="username">@commit.committer</a>
</td>
<td>@commit.shortMessage</td>
<td style="width: 10%; text-align: right;">
<a href="@url(repository)/commit/@commit.id" class="monospace">@commit.id.substring(0, 7)</a>
</td>
</tr>
}
}
</table>
</div>

View File

@@ -0,0 +1,145 @@
@(commits: Seq[Seq[util.JGitUtil.CommitInfo]],
diffs: Seq[util.JGitUtil.DiffInfo],
members: List[String],
originId: String,
forkedId: String,
sourceId: String,
commitId: String,
repository: service.RepositoryService.RepositoryInfo,
originRepository: service.RepositoryService.RepositoryInfo,
forkedRepository: service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Pull Requests - " + repository.owner + "/" + repository.name){
@html.header("pulls", repository)
<div class="pullreq-info">
<div id="compare-info">
<a href="#" id="edit-compare-condition" class="btn btn-mini pull-right">Edit</a>
<span class="label label-info monospace">@originRepository.owner:@originId</span> ... <span class="label label-info monospace">@forkedRepository.owner:@forkedId</span>
</div>
<div id="compare-edit" style="display: none;">
<a href="#" id="cancel-condition-editing" class="pull-right"><i class="icon-remove-circle"></i></a>
@helper.html.dropdown(originRepository.owner + "/" + repository.name, "base fork") {
@members.map { member =>
<li><a href="#" class="origin-owner" data-name="@member">@helper.html.checkicon(member == originRepository.owner) @member/@repository.name</a></li>
}
}
@helper.html.dropdown(originId, "base") {
@originRepository.branchList.map { branch =>
<li><a href="#" class="origin-branch" data-name="@branch">@helper.html.checkicon(branch == originId) @branch</a></li>
}
}
...
@helper.html.dropdown(forkedRepository.owner + "/" + repository.name, "head fork") {
@members.map { member =>
<li><a href="#" class="forked-owner" data-name="@member">@helper.html.checkicon(member == forkedRepository.owner) @member/@repository.name</a></li>
}
}
@helper.html.dropdown(forkedId, "compare") {
@forkedRepository.branchList.map { branch =>
<li><a href="#" class="forked-branch" data-name="@branch">@helper.html.checkicon(branch == forkedId) @branch</a></li>
}
}
</div>
</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>
</div>
<div id="pull-request-form" class="box" style="display: none;">
<div class="box-content">
<form method="POST" action="@path/@originRepository.owner/@repository.name/pulls/new" validate="true">
<div style="width: 260px; position: absolute; margin-left: 635px;">
<div class="check-conflict" style="display: none;">
<img src="@assets/common/images/indicator.gif"/> Checking...
</div>
</div>
<div style="width: 620px; border-right: 1px solid #d4d4d4;">
<span class="error" id="error-title"></span>
<input type="text" name="title" style="width: 600px" placeholder="Title"/>
@helper.html.preview(repository, "", false, true, "width: 600px; height: 200px;")
<input type="hidden" name="targetUserName" value="@originRepository.owner"/>
<input type="hidden" name="targetBranch" value="@originId"/>
<input type="hidden" name="requestUserName" value="@forkedRepository.owner"/>
<input type="hidden" name="requestBranch" value="@forkedId"/>
<input type="hidden" name="commitIdFrom" value="@sourceId"/>
<input type="hidden" name="commitIdTo" value="@commitId"/>
</div>
</form>
</div>
</div>
}
@if(commits.isEmpty){
<table class="table table-bordered table-hover table-issues">
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
<h4>There isn't anything to compare.</h4>
<span class="strong">@originRepository.owner:@originId</span> and <span class="strong">@forkedRepository.owner:@forkedId</span> are identical.
</td>
</tr>
</table>
} else {
@pulls.html.commits(commits, repository)
@helper.html.diff(diffs, repository, Some(commitId), Some(sourceId), true)
}
}
<script>
$(function(){
$('#edit-compare-condition').click(function(){
$('#compare-info').hide();
$('#compare-edit').show();
});
$('#cancel-condition-editing').click(function(){
$('#compare-info').show();
$('#compare-edit').hide();
});
$('a.origin-owner, a.forked-owner, a.origin-branch, a.forked-branch').click(function(){
var e = $(this);
e.parents('ul').find('i').attr('class', 'icon-white');
e.find('i').attr('class', 'icon-ok');
e.parents('div.btn-group').find('button span.strong').text(e.text());
@if(members.isEmpty){
location.href = '@url(repository)/compare/' +
$.trim($('i.icon-ok').parents('a.origin-branch').data('name')) + '...' +
$.trim($('i.icon-ok').parents('a.forked-branch').data('name'));
} else {
location.href = '@url(repository)/compare/' +
$.trim($('i.icon-ok').parents('a.origin-owner' ).data('name')) + ':' +
$.trim($('i.icon-ok').parents('a.origin-branch').data('name')) + '...' +
$.trim($('i.icon-ok').parents('a.forked-owner' ).data('name')) + ':' +
$.trim($('i.icon-ok').parents('a.forked-branch').data('name'));
}
});
$('#show-form').click(function(){
$(this).hide();
$('#pull-request-form').show();
});
@if(hasWritePermission){
function checkConflict(from, to, noConflictHandler, hasConflictHandler){
$('.check-conflict').show();
$.get('@url(repository)/compare/' + from + '...' + to + '/mergecheck',
function(data){ $('.check-conflict').html(data); });
}
@if(members.isEmpty){
checkConflict(
$.trim($('i.icon-ok').parents('a.origin-branch').data('name')),
$.trim($('i.icon-ok').parents('a.forked-branch').data('name'))
);
} else {
checkConflict(
$.trim($('i.icon-ok').parents('a.origin-owner' ).data('name')) + ":" +
$.trim($('i.icon-ok').parents('a.origin-branch').data('name')),
$.trim($('i.icon-ok').parents('a.forked-owner' ).data('name')) + ":" +
$.trim($('i.icon-ok').parents('a.forked-branch').data('name'))
);
}
}
});
</script>

View File

@@ -0,0 +1,69 @@
@(issue: model.Issue,
pullreq: model.PullRequest,
comments: List[model.IssueComment],
collaborators: List[String],
milestones: List[(model.Milestone, Int, Int)],
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
<div class="row-fluid">
<div class="span10">
@issues.html.issuedetail(issue, comments, collaborators, milestones, hasWritePermission, repository)
@issues.html.commentlist(issue, comments, hasWritePermission, repository, Some(pullreq))
@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;">
<div id="merge-pull-request">
<div class="check-conflict" style="display: none;">
<img src="@assets/common/images/indicator.gif"/> Checking...
</div>
</div>
<div id="confirm-merge-form" style="display: none;">
<form method="POST" action="@url(repository)/pull/@issue.issueId/merge">
<div class="strong">
Merge pull request #@issue.issueId from @{pullreq.requestUserName}/@{pullreq.requestBranch}
</div>
<span id="error-message" class="error"></span>
<textarea name="message" style="width: 680px; height: 80px;">@issue.title</textarea>
<div>
<input type="button" class="btn" value="Cancel" id="cancel-merge-pull-request"/>
<input type="submit" class="btn btn-success" value="Confirm merge"/>
</div>
</form>
</div>
</div>
</div>
}
@issues.html.commentform(issue, hasWritePermission, repository)
</div>
<div class="span2">
@if(issue.closed) {
@if(comments.exists(_.action == "merge")){
<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>
}
<div class="small" style="text-align: center;">
<span class="strong">@comments.size</span> @plural(comments.size, "comment")
</div>
<hr/>
</div>
</div>
<script>
$(function(){
$('#cancel-merge-pull-request').click(function(){
$('#confirm-merge-form').hide();
$('#merge-pull-request').show();
});
@if(hasWritePermission){
$('.check-conflict').show();
$.get('@url(repository)/pull/@issue.issueId/mergeguide',
function(data){ $('.check-conflict').html(data); });
}
});
</script>

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