Compare commits

...

667 Commits
1.2 ... 1.10

Author SHA1 Message Date
takezoe
b885a1a0d4 (refs #256)If account is already registered but disabled, authentication fails. 2014-02-01 17:05:33 +09:00
takezoe
1705bd3ae9 Merge remote-tracking branch 'origin/master' 2014-02-01 07:08:39 +09:00
takezoe
e87c69f989 (refs #251)Remove BOM from UTF-8 string. 2014-02-01 07:08:03 +09:00
takezoe
1c529eea3d Disable the post commit hook for Wiki repository. 2014-02-01 07:06:17 +09:00
takezoe
738b0cfe9a Add version 1.10. 2014-02-01 06:11:18 +09:00
takezoe
913561cb2a (refs #254)Remove AUTO_SERVER=TRUE for performance issue. 2014-01-30 20:42:53 +09:00
takezoe
79827efe9b Merge branch 'master' of https://github.com/takezoe/gitbucket 2014-01-28 03:20:25 +09:00
takezoe
8722cd89fc Merge branch 'ldap-fullname' 2014-01-28 03:19:40 +09:00
Naoki Takezoe
52fcc4ad1e Update README.md 2014-01-26 08:43:34 +09:00
takezoe
59a096bfd6 (refs #250)Include repository name in download zip filename. 2014-01-25 05:25:17 +09:00
takezoe
5a1f541e13 (refs #245)Add full name attribute for LDAP authentication. 2014-01-25 05:07:32 +09:00
takezoe
94bd1c6a93 Merge branch 'rename-repository' 2014-01-18 18:05:45 +09:00
takezoe
5b1aef5e52 (refs #101, #102)Put Repository deletion and transfer ownership together to Danger Zone. 2014-01-18 07:06:48 +09:00
takezoe
89bfcdc44e (refs #102)Add validation and auto completion to the transfer user name field 2014-01-18 06:44:39 +09:00
takezoe
fba81138ea (refs #102)Experimental implementation of transfer repository ownership 2014-01-18 04:14:32 +09:00
takezoe
d50e07265e Add unique checking before rename repository. 2014-01-18 03:54:57 +09:00
takezoe
c92891538e Fix Cancel button position in the comment editing form. 2014-01-18 01:52:59 +09:00
takezoe
ccc1e9bc8b (refs #246)Fix issue editing 2014-01-18 01:52:34 +09:00
takezoe
f33b398428 (refs #102)Change for transfer repository owner. 2014-01-16 19:39:14 +09:00
takezoe
226a8af262 Use old home if it exists. 2014-01-15 05:25:18 +09:00
takezoe
ebcc5ab4b1 (refs #101)Update links in activity message also. 2014-01-15 04:35:38 +09:00
takezoe
10e16e8379 Use old home if it exists. 2014-01-15 01:28:15 +09:00
takezoe
df1f3d8a00 (refs #101)Modification to add rename repository name. 2014-01-13 02:09:05 +09:00
takezoe
5e2dfffe25 Use FileUtils#moveDirectory() instead of File#renameTo() 2014-01-12 17:59:02 +09:00
takezoe
897f2ea6dd Update data directory checking condition. 2014-01-12 16:47:23 +09:00
takezoe
3ff39ec578 Merge SignInController into IndexController 2014-01-12 16:16:09 +09:00
takezoe
3d852a535d (refs #244)Change the default data directory to HOME/.gitbucket 2014-01-12 15:41:01 +09:00
takezoe
6f6a61f31a Fix pattern match for webhook. 2014-01-04 17:23:18 +09:00
takezoe
10f54f5790 Ignore .settings directory. 2014-01-04 04:16:51 +09:00
takezoe
0e7280585a Fix refs commit log and web hook. 2014-01-04 04:11:41 +09:00
takezoe
1da7173f27 Merge branch 'master' of https://github.com/takezoe/gitbucket 2013-12-28 02:43:57 +09:00
takezoe
1cb1e68a01 Add Version 1.9 2013-12-28 02:43:15 +09:00
Naoki Takezoe
b59c8a5512 Update README.md 2013-12-28 01:49:04 +09:00
Naoki Takezoe
fe63ad0976 Merge pull request #242 from ssogabe/pull-request-messages
Fixed pull request messages.They are a bit different from GitHub
2013-12-21 19:57:50 -08:00
ssogabe
941cb7b851 Fixed pull request messages.They are a bit different from GitHub 2013-12-22 12:28:36 +09:00
takezoe
d1cf0d9fd7 (refs #238)Enable automatic mixed mode of H2. 2013-12-22 02:06:44 +09:00
takezoe
64c2bb4d6b (refs #237)Add link to gitbucket.plist 2013-12-21 05:30:48 +09:00
takezoe
24c9f5c17e (refs #237)Move gitbucket.plist to contrib. 2013-12-21 05:29:52 +09:00
Naoki Takezoe
d368e4e80d Merge pull request #237 from hanxue/osx
Add plist launcher for OS X and installation instructions
2013-12-20 12:26:32 -08:00
Naoki Takezoe
5c0ff84fc4 Merge pull request #233 from shootaroo/fix-style-readme
Fix style markdown table on README.md
2013-12-20 11:51:08 -08:00
Lee Hanxue
502a21b6b6 Add plist launcher for OS X and installation instructions 2013-12-19 17:59:32 +08:00
takezoe
0e9bf59c0f Remove some functions from ControlUtil. 2013-12-15 04:21:39 +09:00
shootaroo
108f9fccdd Fix style markdown table on README.md 2013-12-13 19:28:10 +09:00
takezoe
ac884bd7c3 (refs #196)Fire WebHook in merging pull request from Web GUI. 2013-12-12 04:23:43 +09:00
takezoe
a4cb5c991c Upgrade scalatra-forms to 0.0.11. 2013-12-12 03:22:37 +09:00
takezoe
68f1f55f37 (refs #223)Display GITBUCKET_HOME on the system settings. 2013-12-12 03:17:42 +09:00
Naoki Takezoe
1dc779d5e8 Merge pull request #228 from mcveat/compiler-warnings
Silenced compiler warnings
2013-12-11 09:53:19 -08:00
Naoki Takezoe
f781c7a08c Merge pull request #220 from maliayas/patch-1
Turn off autocomplete on "Add collaborator" form
2013-12-03 12:13:20 -08:00
Naoki Takezoe
a8511a9f39 Merge pull request #221 from drwlrsn/master
Fixes issue #216
2013-12-03 12:08:45 -08:00
Piotr Adamski
47714eec45 Silenced compiler warnings 2013-12-03 18:41:41 +01:00
takezoe
c46e9b2f4d (refs #204)Replace order of ScalatraListener and AutoUpdateListener 2013-12-03 02:16:00 +09:00
Drew Larson
26d579f13f Fixes issue #216
Added a div element to wrap the buttons so they are vertically aligned with each other. Also converted input and a elements to button elements as Bootstrap recommends: http://getbootstrap.com/css/#buttons-tags
2013-12-01 19:56:21 -06:00
Ali Ayas
6556d26742 Turn off autocomplete on "Add collaborator" form
You have already created js autocomplete for that input, so it is good to turn off the browser autocomplete. If there are more forms that have custom autocomplete, this change should be applied to them, too.
2013-12-01 23:55:34 +02:00
Naoki Takezoe
608dce2205 Update README.md 2013-11-30 21:20:12 +09:00
Naoki Takezoe
f86e50c723 Update README.md 2013-11-30 21:04:21 +09:00
Naoki Takezoe
b60fe33886 Update README.md 2013-11-30 20:26:31 +09:00
Naoki Takezoe
5210a143fd Update README.md 2013-11-30 20:22:00 +09:00
takezoe
6b11c1a180 (refs #204)Change the option name from --data to --gitbucket.home 2013-11-30 05:22:35 +09:00
takezoe
b3669f6d66 (refs #204)Add some way to configure data directory.
1) system property of JVM (e.g. -Dgitbucket.home=PATH_TO_DATADIR)
2) java -jar gitbucket.war --data=PATH_TO_DATADIR
3) Add context parameter "gitbucket.home" to web.xml
2013-11-30 05:18:15 +09:00
takezoe
bbff75e037 (refs #211)README.md detection is case insensitive and it also detect README.markdown as same as Github. 2013-11-30 04:22:19 +09:00
takezoe
7e10618ceb Merge branch 'pullreq-update-in-push' of https://github.com/odz/gitbucket into odz-pullreq-update-in-push
Conflicts:
	src/main/scala/app/PullRequestsController.scala
2013-11-30 02:35:21 +09:00
takezoe
7f4def6b83 Ignore Exception instead of TransportException 2013-11-25 18:21:59 +09:00
takezoe
5790d246c8 Ignore TransportException if the source branch had been removed. 2013-11-25 18:16:38 +09:00
Naoki Takezoe
19dee09c86 Merge pull request #203 from olivierdagenais/FixManualMergeUrls
Remove superfluous context.path
2013-11-22 12:27:43 -08:00
Naoki Takezoe
dfe2889912 Merge pull request #202 from odz/delete-repository-with-pullreq
Resolves Error when deleting repository which has PR.
2013-11-21 06:40:19 -08:00
odz
223ba791fe Fetch pull request from source repository after updating repository. 2013-11-20 23:59:26 +09:00
odz
0d49bbe7ac Resolves error when deleting repsository with PR. 2013-11-20 01:53:18 +09:00
Olivier Dagenais
8381e8122a Remove superfluous context.path
It was generating URLs that look like
http://server.example.com/gitbucket/gitbucket/git/user/repo.git (notice
the extra "/gitbucket"?) when the WAR was deployed in Tomcat.
2013-11-19 11:48:29 -05:00
Naoki Takezoe
f38924c7fe Merge pull request #190 from jtyr/master
Adding LDAP StartTLS support
2013-11-15 09:10:38 -08:00
takezoe
43152c9341 Upgrade scalatra-forms to 0.0.8. 2013-11-14 04:04:29 +09:00
takezoe
cf84e8b7cc (refs #173)Move BasicAuthenticationFilter to ScalatraBootstrap also. 2013-11-12 00:34:11 +09:00
takezoe
2b42e73530 (refs #173)Move BasicAuthenticationFilter to ScalatraBootstrap also. 2013-11-11 03:12:41 +09:00
Naoki Takezoe
60030959f2 Merge pull request #194 from jtyr/gravatar_https
Load Gravatar images always through HTTPS
2013-11-09 23:09:18 -08:00
Jiri Tyr
7174523ac5 Load Gravatar images always through HTTPS
This patch will force to load Gravatar images always through HTTPS which
will fix the problem with mixed content when accessing the page through
HTTPS.

The problem is that if an HTTPS page includes HTTP content, the HTTP
portion can be read or modified by attackers, even though the main page
is served over HTTPS.
2013-11-10 00:42:17 +00:00
takezoe
f573fef9eb (refs #173)Move TransactionFilter to ScalatraBootstrap from web.xml to support Tomcat 7.0.29 or before. 2013-11-10 05:24:38 +09:00
takezoe
b4250d8254 Merge remote-tracking branch 'origin/master' 2013-11-10 02:46:22 +09:00
takezoe
ac4d4de3c1 Fix redirect path encoding. 2013-11-10 02:45:54 +09:00
Naoki Takezoe
05e6d008fa Merge pull request #192 from xuwei-k/issue191
add HARDWRAPS option
2013-11-09 09:17:26 -08:00
takezoe
dd4abb2073 Upgrade to scalatra-forms 0.0.6. 2013-11-08 03:24:12 +09:00
Jiri Tyr
612aba1365 Use the system keystore by default
Default system keystore is in:
$JAVA_HOME/lib/security/jssecacerts
or in:
$JAVA_HOME/lib/security/cacerts

Custom keystore can be set either in /etc/sysconfig/gitbucket by
specifying the following option:
GITBUCKET_JVM_OPTS="-Djavax.net.ssl.trustStore=/path/to/your/cacerts"
or in Gitbucket's System Settings.
2013-11-07 16:57:40 +00:00
xuwei-k
94dce09570 add HARDWRAPS option 2013-11-07 17:27:18 +09:00
Jiri Tyr
cc241c5a7b Moving keystore definition into settings 2013-11-05 15:08:03 +00:00
shimamoto
13cf9d01f0 (refs #181) Fixed bug in charset. 2013-11-04 04:04:14 +09:00
takezoe
47453fec3f (refs #189)Fix Wiki page editing via redirecting from unexisting page. 2013-11-03 18:26:06 +09:00
takezoe
641d506559 (refs #177)Fix regular expressions for issue link conversion. 2013-11-03 18:06:58 +09:00
takezoe
3dec2b8159 Fix test case. 2013-11-03 15:12:01 +09:00
takezoe
a0bd969140 (refs #114)Add functionality to remove account by themselves. 2013-11-03 14:54:28 +09:00
takezoe
b30d42a37b (refs #114)Remove unnecessary database accessing. 2013-11-03 14:51:42 +09:00
takezoe
a03acc68e7 (refs #114)Disable link for disabled users. 2013-11-03 14:32:03 +09:00
takezoe
05296473d3 (refs #114)Bug fix 2013-11-03 04:53:41 +09:00
takezoe
2118f8c764 (refs #114)Add group deletion. 2013-11-03 01:37:23 +09:00
takezoe
e366af98b5 (refs #114)Add TODO to helpers#activityMessage() 2013-11-02 14:10:05 +09:00
takezoe
81e2ac44c3 (refs #114)Remove user related data when user is removed. 2013-11-02 14:01:07 +09:00
takezoe
07bb326c06 (refs #114)Add remove option to user management. 2013-11-02 13:44:47 +09:00
takezoe
bcc2c8cc2d Fix test case. 2013-11-02 05:20:24 +09:00
takezoe
2e0e17f1aa (refs #185)Add -Dsbt.log.noformat=true option 2013-11-02 05:12:36 +09:00
takezoe
c517b44e82 Upgrade to scalatra-forms 0.0.4 2013-11-02 05:07:09 +09:00
Jiri Tyr
f311339786 Adding LDAP StartTLS support
Some LDAP server do not allow authenticate with unencrypted password.
This patch is adding the StartTLS support which takes care of the
encryption.

In order to enable the StartTLS, go to "System Settings" and select the
"Enable StartTLS" in the Authentication section. Then make sure that you
add your LDAP certificate into the Java keystore:

$ keytool -import \
          -file /etc/pki/tls/certs/cacert.pem \
          -alias myName \
          -keystore /var/lib/gitbucket/keystore

You can list all keys from the keystore like this:

$ keytool -list -keystore /var/lib/gitbucket/keystore
2013-11-01 15:44:19 +00:00
takezoe
34853d0322 Fix test case. 2013-11-01 12:42:09 +09:00
takezoe
9c60b69c88 (refs #114)Add logical remove flag to ACCOUNT. 2013-11-01 03:39:59 +09:00
takezoe
4f10bccf84 (refs #114)Add logical remove flag to ACCOUNT. 2013-11-01 03:38:33 +09:00
takezoe
c7eaebf597 (refs #186)Show private repositories in the account page. 2013-11-01 03:25:06 +09:00
takezoe
60e1052d33 (refs #179)Fetch from the source repository before pull request is referred. 2013-11-01 03:12:56 +09:00
Tomofumi Tanaka
7e77c102b0 (refs #179) Merge branch 'improve-pullreq-performance' 2013-10-31 22:15:47 +09:00
takezoe
a452c582ab Fix compilation error. 2013-10-31 03:08:28 +09:00
takezoe
0d3adb074d Release ObjectInserter after adding commit. 2013-10-31 02:18:48 +09:00
takezoe
8ec4b52dda (refs #167)Add pusher info to WebHook 2013-10-31 02:07:54 +09:00
Tomofumi Tanaka
9265c68383 (refs #179) Refactor 2013-10-31 01:38:29 +09:00
Tomofumi Tanaka
4bd2d78ecb Merge remote-tracking branch 'master' into improve-pullreq-performance 2013-10-31 00:56:18 +09:00
Tomofumi Tanaka
e7aa766d0a (refs #179) Remove refs/pull/${issueId}/merge 2013-10-31 00:50:27 +09:00
Tomofumi Tanaka
7d8300b3ce (refs #179) Fetch from fork branch before merge 2013-10-31 00:30:18 +09:00
Tomofumi Tanaka
af8a1234ed (refs #179) Improve merge performance
* merge without tmp dir repository
* remove merging ops from mergeguid
2013-10-31 00:23:09 +09:00
takezoe
bd0ecd0a9d Improve repository creation to not use the working repository. 2013-10-30 14:52:55 +09:00
takezoe
35c8f02f90 (refs #180)Fix compilation error. 2013-10-30 13:22:54 +09:00
takezoe
f160952817 Remove unused import statement. 2013-10-30 13:20:13 +09:00
takezoe
9e5a302ab1 (refs #180)Fix a problem about multi-byte characters. 2013-10-30 13:19:25 +09:00
takezoe
a1dc19fa26 (refs #180)Remove Directory#getWikiWorkDir() 2013-10-30 11:39:55 +09:00
takezoe
e79ded934f Display selected page differences only. 2013-10-30 11:37:53 +09:00
takezoe
ef3e7d9286 (refs #180)Reverting from history without working repository is completed. 2013-10-30 11:13:10 +09:00
takezoe
68b25ddbb5 (refs #180)Implementing reverting from history without ApplyCommand. 2013-10-30 08:20:17 +09:00
Tomofumi Tanaka
f96040eade Improve checkConflict
* Delete temporary RefSpec after checkConflict
* mergeguide use checkconflictInPullRequest instead of checkconflict
2013-10-30 01:33:56 +09:00
takezoe
599a808054 Fix a link to the committer page. 2013-10-29 11:53:05 +09:00
takezoe
382c5c55ec Remove unused import statement. 2013-10-29 11:52:42 +09:00
takezoe
afb2306904 (refs #180)Fix saving and deleting Wiki page. 2013-10-29 11:39:38 +09:00
takezoe
2642da3be3 (refs #180)Eliminating the working repository cloning in Wiki. 2013-10-29 09:22:37 +09:00
Tomofumi Tanaka
dcbf283c9d Improve checkConflict 2013-10-29 01:24:06 +09:00
Naoki Takezoe
f38fa0132c Merge pull request #178 from jtyr/master
Version bump in spec file
2013-10-28 07:15:07 -07:00
Jiri Tyr
569053f7e0 Version bump in spec file 2013-10-28 11:18:58 +00:00
Naoki Takezoe
037a97ff3d Update README.md 2013-10-28 11:28:08 +09:00
Naoki Takezoe
6e169ab3c2 Update README.md 2013-10-26 01:46:39 +09:00
Naoki Takezoe
6ac27e89b3 Update README.md 2013-10-26 01:45:59 +09:00
takezoe
2235dab550 (refs #174)Fix commit hook for DELETE command. 2013-10-26 01:40:26 +09:00
Naoki Takezoe
7604c2172f Update README.md 2013-10-25 04:44:03 +09:00
takezoe
1e750f4b9d (refs #171)Fix link target of pull request number. 2013-10-25 04:28:58 +09:00
takezoe
d1f0d01ae8 Small fix for #170 2013-10-25 04:05:34 +09:00
Naoki Takezoe
167a0f28b2 Merge pull request #170 from jtyr/master
Allow to force HTTPS scheme
2013-10-24 12:00:25 -07:00
takezoe
06be5266fd Fix refs message. 2013-10-24 15:04:32 +09:00
takezoe
60e7165983 (refs #104)Zip file is exported from the bare repository directly. 2013-10-24 11:29:23 +09:00
takezoe
6dbfc12896 Merge branch 'master' of https://github.com/takezoe/gitbucket 2013-10-24 11:07:26 +09:00
takezoe
6d4b3e54d0 Fix style problem in Wiki. 2013-10-24 11:03:47 +09:00
takezoe
2968b92677 Add the commit link to refs comment. 2013-10-24 04:25:50 +09:00
takezoe
0d0bf4ad3f Don't add group account as a collaborator. 2013-10-24 02:08:14 +09:00
takezoe
53fa60b0f8 Exclude owner from assigned user list in the group repository. 2013-10-24 02:00:14 +09:00
Jiri Tyr
99517fa508 Fixing command line options in init.d script
Adding missing "--host" option and fixing the declaration of other
command line options.
2013-10-21 22:47:27 +01:00
Jiri Tyr
2e239d16d4 Refactorization of the https command line option
Renaming the flag variable and the Connector class.
2013-10-21 22:45:34 +01:00
Jiri Tyr
6de5babd5b Merge remote-tracking branch 'upstream/master' 2013-10-21 17:40:42 +01:00
takezoe
f3ad1a019d Small fix for #147 2013-10-22 01:09:22 +09:00
Naoki Takezoe
90ab882e8e Merge pull request #147 from xuwei-k/AccountServiceSpec
add AccountServiceSpec
2013-10-21 09:01:11 -07:00
Jiri Tyr
53269096a6 Allow to force HTTPS scheme
If the standalone GitBucket instance runs behind HTTPS proxy, the
repository URL always shows HTTP scheme dispute the fact that the
connection is HTTPS. This patch is adding a command line option which
allows to force the HTTPS scheme.
2013-10-21 14:32:53 +01:00
shimamoto
254509f243 Fix bug. 2013-10-21 22:32:46 +09:00
Naoki Takezoe
a697f186af Merge pull request #164 from smly/feature/bugfix-redirect-encode
Fix a problem to redirect wikipage named by multi-byte characters
2013-10-20 19:33:37 -07:00
takezoe
2316a80be9 (refs #163)Remove escaping for '.' 2013-10-21 10:53:36 +09:00
shimamoto
bbcb04b263 Fix bug. 2013-10-21 05:16:55 +09:00
shimamoto
7afe7fbb5f (refs #103) Add issue comment deletion. 2013-10-21 05:11:53 +09:00
takezoe
7c7da7379d (refs #163)Fixed 2013-10-19 18:48:50 +09:00
Kohei Ozaki
37358e9c8c Fix a problem to redirect wikipage named by multi-byte characters
In some specific case, redirect path (created from route params) is incorrect.
`redirectUrl` is expected to be encoded,
but scalatra decodes route params by rl.UrlCodingUtils via ScalatraBase.UriDecoder.
To avoid this problem, I add dirty workaround to encode redirect path.
2013-10-19 14:20:35 +09:00
Naoki Takezoe
41941df87a Update README.md 2013-10-19 13:58:04 +09:00
takezoe
bf2ed81eb1 (refs #163)Allow '.' in user name. 2013-10-19 12:56:55 +09:00
Naoki Takezoe
2d85d41e9c Merge pull request #161 from jtyr/master
System support for RedHat
2013-10-17 10:36:09 -07:00
Naoki Takezoe
e5e7b2484c Describe version of Servlet container 2013-10-18 00:59:16 +09:00
Jiri Tyr
6058552654 System support for RedHat
This commit is adding init.d script and sysconfig file which allows to
run GitBucket in the standalone mode. It also adds the spec file which
allows to build RPM package.
2013-10-17 01:39:47 +01:00
takezoe
f40c7ff4fa Fix testcase. 2013-10-16 04:47:43 +09:00
takezoe
da62c6181e (refs #33)Fix avatar icon and account link in the commits page for pull request. 2013-10-16 04:10:00 +09:00
takezoe
4d066738eb Merge branch 'master' into #33_match-by-email
Conflicts:
	src/main/scala/view/AvatarImageProvider.scala
2013-10-16 02:34:12 +09:00
Naoki Takezoe
cb12d03262 Merge pull request #143 from chris-vanvranken/master
retrieve LDAP emails as lowercase to avoid confusion
2013-10-15 10:28:05 -07:00
takezoe
9a6a2d9b78 (refs #33)Improve avatar image searching behavior. 2013-10-16 02:24:40 +09:00
takezoe
ff0af477cb Merge branch 'master' into #33_match-by-email
Conflicts:
	src/main/scala/view/helpers.scala
2013-10-16 01:51:44 +09:00
Chris Van Vranken
05adf9345f retrieve LDAP emails as lowercase to avoid confusing gravatar 2013-10-14 13:00:29 -04:00
Naoki Takezoe
ba70fdda48 Merge pull request #156 from xuwei-k/deprecated
fix deprecation warning
2013-10-14 06:42:35 -07:00
Naoki Takezoe
3885fcb2ec Merge pull request #157 from ssogabe/failonerror
Abort build process if sbt reports errors
2013-10-14 06:42:18 -07:00
ssogabe
99800a27f5 Abort build process if sbt reports errors 2013-10-14 15:01:30 +09:00
takezoe
107622942b (refs #127)Fix testcase. 2013-10-14 14:54:38 +09:00
xuwei-k
9794f14a65 fix deprecation warning. use HttpClientBuilder
https://github.com/apache/httpclient/blob/4.3/httpclient/src/main/java-deprecated/org/apache/http/impl/client/DefaultHttpClient.java#L113
2013-10-14 14:52:42 +09:00
xuwei-k
af759a815f add scalacOptions 2013-10-14 14:49:52 +09:00
takezoe
0e7078c479 (refs #151)Add unique checking to group creation. 2013-10-14 14:44:26 +09:00
Naoki Takezoe
83107c7974 Merge pull request #155 from HairyFotr/patch-2
Add plugin, Update versions
2013-10-13 22:13:16 -07:00
takezoe
ff9b2dbe93 (refs #127)Display full name at the account page. 2013-10-14 14:05:25 +09:00
takezoe
ebf4e5f2e9 Merge branch 'account-full-name' of https://github.com/robinst/gitbucket into robinst-account-full-name
Conflicts:
	src/main/scala/app/PullRequestsController.scala
2013-10-14 13:40:06 +09:00
Naoki Takezoe
21c30583e5 Merge pull request #153 from xuwei-k/number-format-exception
avoid NumberFormatException
2013-10-13 21:32:52 -07:00
takezoe
d6c9ace306 Merge branch 'master' of https://github.com/takezoe/gitbucket 2013-10-14 13:25:01 +09:00
takezoe
faf1252597 (refs #154)Remove comparison for original repository which does not exist. 2013-10-14 13:24:49 +09:00
HairyFotr
7b2ee25ea2 Add plugin, Update versions 2013-10-13 21:25:44 +02:00
xuwei-k
5a3207ae42 avoid NumberFormatException 2013-10-14 03:26:47 +09:00
Naoki Takezoe
3eab4955b9 Merge pull request #152 from xuwei-k/avoid-option-get
refactoring. avoid Option#get
2013-10-13 09:41:09 -07:00
xuwei-k
d772fc3ba2 refactoring. avoid Option#get 2013-10-14 01:18:31 +09:00
Naoki Takezoe
7de0a3fd70 Fix link to IIS installation wiki page. 2013-10-13 10:47:06 +09:00
Naoki Takezoe
eb8710a336 Merge pull request #144 from chris-vanvranken/patch-1
Add note that installation on windows server IIS is possible
2013-10-12 18:45:30 -07:00
Naoki Takezoe
25c55ecbd0 Merge pull request #150 from ssogabe/specs2
configure sbt to use the junitxml option with specs2
2013-10-12 18:31:13 -07:00
ssogabe
280df2cedd configure sbt to use the junitxml option with specs2 2013-10-13 09:38:52 +09:00
Naoki Takezoe
5ba9c86bee Merge pull request #149 from jparound30/activity_problem
Fix activity message problem (related to #120)
2013-10-12 08:14:13 -07:00
jparound30
faa6591d27 Fix activity message problem (related to #120) 2013-10-12 20:50:35 +09:00
takezoe
841d442f0d (refs #131)Don't create Dropzone if image has been registered. 2013-10-12 02:54:33 +09:00
xuwei-k
3351eabc4f add AccountServiceSpec 2013-10-11 13:08:58 +09:00
takezoe
006e1bc61e Merge branch 'master' of https://github.com/takezoe/gitbucket 2013-10-11 03:10:10 +09:00
takezoe
35d1b4ea37 (refs #142)Hide "Fork" button for repositories which have no commit. 2013-10-11 03:09:25 +09:00
Naoki Takezoe
b0c5069695 Merge pull request #145 from xuwei-k/refactoring
refactoring
2013-10-10 11:01:26 -07:00
xuwei-k
dae0d0ad4b use FileUtil.withTmpDir and FileUtil.using 2013-10-11 02:44:03 +09:00
xuwei-k
79e560b7bf add FileUtil.withTmpDir 2013-10-11 02:44:03 +09:00
takezoe
cf79ac1069 (refs #142)Fix NoSuchElementException for empty repository. 2013-10-11 02:42:07 +09:00
Chris Van Vranken
8aab7a16c4 Add note that installation on windows server IIS is possible 2013-10-10 12:41:10 -04:00
shimamoto
c16b89b0be (refs #99) Improved to configure the FROM field of the email. 2013-10-10 23:57:45 +09:00
takezoe
25bbc00ff3 Display repository search field on pull request pages. 2013-10-10 19:00:56 +09:00
Tomofumi Tanaka
e667b6c139 (refs #139) Add info log for debugging LDAP Auth 2013-10-10 00:57:46 +09:00
Naoki Takezoe
195364223f Merge pull request #140 from xuwei-k/remove-unnecessary-code
remove unnecessary code
2013-10-08 21:17:50 -07:00
xuwei-k
84ce2cac8d remove unnecessary code 2013-10-09 11:09:06 +09:00
takezoe
f3507cf465 (refs #117)Add build.properties to fix sbt version. 2013-10-09 10:44:52 +09:00
Tomofumi Tanaka
f74f2c47d3 (refs #120) URL encode tag name
URL encode tag name URL like branch name.
And rename encodeBranchName to encodeRefName.
2013-10-09 09:59:30 +09:00
takezoe
72b25591a5 Resolve length issue in Slick.
https://github.com/slick/slick/issues/170
2013-10-09 03:11:27 +09:00
Naoki Takezoe
fe23a5c6da Merge pull request #136 from talios/patch-1
Change link text to 'Older'
2013-10-07 18:21:30 -07:00
Mark Derricutt
49fbc5cb62 Change link text to 'Older'
Updates the link text for previous commits to read 'Older' and not 'Order'.
2013-10-08 11:34:02 +13:00
takezoe
5a19a307a9 Merge remote-tracking branch 'origin/master' 2013-10-08 03:08:06 +09:00
takezoe
c3ec52b391 (refs #128)Add javacOptions to specify 6 as compilation target. 2013-10-08 03:07:33 +09:00
Naoki Takezoe
f2d68be0a3 Merge pull request #134 from tanacasino/fix-link-use-urlencode
(refs #120) Use encodeBranchName to tab links
2013-10-07 09:16:52 -07:00
Tomofumi Tanaka
c1f98ac481 (refs #120) Use encodeBranchName to tab links 2013-10-07 23:55:27 +09:00
Naoki Takezoe
8287c84dc7 Merge pull request #126 from robinst/readme-padding
Add padding around repository readme content
2013-10-06 18:42:39 -07:00
Robin Stocker
13bff2963e Add full name to account and use it to create commits (#125)
The Git practice is to use the full name when creating commits, not a
user name. This commit fixes that by introducing a fullName field to
Account and using it when creating commits.

For migrating from earlier versions, the user name is used as an initial
value for the full name field.
2013-10-06 23:11:09 +02:00
Robin Stocker
035f3f9e02 Add padding around repository readme content
It looks quite bad without padding.
2013-10-06 22:12:03 +02:00
takezoe
65e6de5ba4 (refs #120)URL encode branch name except '/'. 2013-10-07 02:36:35 +09:00
takezoe
82ced9233a Remove debug code. 2013-10-06 23:23:04 +09:00
takezoe
e94411ebeb (refs #121)Create WebHookPayload only when web hook has been registered. 2013-10-06 23:22:29 +09:00
takezoe
b92b429ffa (refs #121)Configure maxIdleTime and soLingerTime. 2013-10-06 21:31:55 +09:00
takezoe
e457cfb212 Fix branch name. 2013-10-06 19:49:46 +09:00
takezoe
f1476c52e6 (refs #121)Optimize push performance for a lot of commit. 2013-10-06 18:31:09 +09:00
takezoe
332246aed6 Add testcase for AvatarImageProvider. 2013-10-06 17:40:10 +09:00
takezoe
1c5201dcf1 Merge branch 'buildXML' of https://github.com/ssogabe/gitbucket 2013-10-06 16:03:25 +09:00
takezoe
36880ace27 (refs #116)Add --host option to bind Jetty connector to the specified hostname. 2013-10-06 15:56:47 +09:00
Naoki Takezoe
0d55d6ef6b Merge pull request #122 from lucas-clemente/patch-1
Fix typo when assigning issues
2013-10-05 09:51:50 -07:00
Lucas Clemente
688bf645b4 Fix typo when assigning issues 2013-10-05 14:51:26 +02:00
takezoe
d5a14482a6 Fix for pull request #119 to take some part of design fix. 2013-10-05 12:58:26 +09:00
Jan-Henrik Bruhn
cc1e0030df Did a lot of design-optimizations
mainly added icons and set correct bootstrap classes for forms, but also used some new fonts, provided via google webfonts
2013-10-05 00:21:10 +02:00
takezoe
fcadcb34a2 Add testcase for Pagination. 2013-10-05 04:31:45 +09:00
takezoe
dd8f440be0 Add testcase. 2013-10-05 03:39:46 +09:00
takezoe
17bc422e7a (refs #84)Add jquery.elastic and apply to issue and comment textarea. 2013-10-04 09:32:32 +09:00
takezoe
380cdbcf75 Add FileUtil#getContentType(). 2013-10-04 04:17:30 +09:00
takezoe
f4f2bf34fc (refs #73)Add Wiki conflict detection and some fix. 2013-10-04 03:48:51 +09:00
Naoki Takezoe
ed713d80a9 Merge pull request #110 from tanacasino/fix/migration-skip
Fix bug dot not skip migration when first init
2013-10-03 09:41:13 -07:00
Tomofumi Tanaka
c39703c61c Fix bug dot not skip migration when first init 2013-10-03 21:58:44 +09:00
takezoe
537773f975 Add testcase example. 2013-10-03 14:02:54 +09:00
takezoe
f37eca7c61 (refs #109)Change link color for absent Wiki pages. 2013-10-03 13:49:09 +09:00
takezoe
40a52d5ad5 Clone Wiki working repository if it does not exist before reverting. 2013-10-03 13:48:31 +09:00
takezoe
d95bd20cbe Fix commit message for Wiki editing. 2013-10-03 11:44:15 +09:00
takezoe
70ca98d6a2 (refs #38)Add reverting wiki from history. 2013-10-03 03:42:38 +09:00
takezoe
cf7caf55da (refs #108)Add ZIP download button to the repository viewer tab. 2013-10-03 00:57:17 +09:00
takezoe
b74bff3b2e Add org.h2.Driver.load(). 2013-10-02 11:03:30 +09:00
takezoe
b2e4853976 Merge branch 'master' of https://github.com/takezoe/gitbucket 2013-10-02 03:31:28 +09:00
takezoe
aef3c5c121 (refs #106)Don't use DbStarter because GitBucket does not use tcp server and it also create connection for each transaction. 2013-10-02 03:31:01 +09:00
takezoe
4afbfcb016 (refs #106)Skip migration if the current version is illegal. 2013-10-02 02:48:49 +09:00
Naoki Takezoe
09f8cff4c9 Update README.md 2013-10-01 03:58:40 +09:00
Naoki Takezoe
9ecd162040 GitBucket 1.6 released. 2013-10-01 03:56:48 +09:00
Naoki Takezoe
8617f02b01 Update README.md 2013-10-01 03:56:19 +09:00
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
ssogabe
9c14ddda18 allow users to build a war on Linux 2013-09-27 22:13:23 +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
fd84b3f1c4 Fix link path in dashborad. 2013-07-31 12:04:09 +09:00
Naoki Takezoe
9d4a052ecc Update for 1.4 release. 2013-07-31 00:49:27 +09:00
takezoe
f93c8965be Bug fix. 2013-07-30 22:03:48 +09:00
takezoe
beef86ce8c Extend session timeout to 24 hours. 2013-07-30 21:41:47 +09:00
shimamoto
03b75d5379 (refs #26) Fix splitWith condition. 2013-07-30 21:08:38 +09:00
shimamoto
66855e65bb (refs #26) Implements repository filter. 2013-07-30 19:36:20 +09:00
takezoe
b8da93912f Fix query in RepositoryService#getVisibleRepositories fluently :-) 2013-07-30 12:42:42 +09:00
takezoe
d675115615 Remove unnecessary <hr>. 2013-07-30 11:59:47 +09:00
shimamoto
0296a0bde6 Merge branch 'master' of https://github.com/takezoe/gitbucket.git
Conflicts:
	src/main/scala/app/DashboardController.scala
2013-07-30 10:51:44 +09:00
shimamoto
8409384232 (refs #26) Implements the dashboard issue display. 2013-07-30 10:47:46 +09:00
Naoki Takezoe
cfaee56a08 Merge pull request #55 from tanacasino/fix/improve-repository-viewer
Make more fast and github-like repository viewer
2013-07-29 15:20:00 -07:00
takezoe
7d65717784 The method of RepositoryService was cleaned up. 2013-07-30 03:41:47 +09:00
Tomofumi Tanaka
7079d50fdf Make more fast and github-like repository viewer 2013-07-30 03:26:36 +09:00
takezoe
41a613e151 Move private method. 2013-07-30 02:45:14 +09:00
takezoe
1f2b6a0acc Adjust whitespaces. 2013-07-29 22:58:30 +09:00
takezoe
25d402c9d1 (refs #53)Fix path extraction for branch which contains '/'. 2013-07-29 22:57:57 +09:00
takezoe
045b7cf019 Fix avatar problem. 2013-07-29 17:10:45 +09:00
takezoe
57109dd72e Add init-param 'webAllowOthers' to web.xml. 2013-07-29 13:24:01 +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
1b878b59b8 Use ISSUE_OUTLINE_VIEW to retrieve comment count. 2013-07-26 02:59:51 +09:00
Naoki Takezoe
80452ab4cd Merge pull request #52 from tanacasino/fix/set-initial-commiter
Initial commiter should be repository creator
2013-07-25 10:36:50 -07:00
Tomofumi Tanaka
4d9c8e8d3e Initial commiter should be repository creator
like wiki repository.
Now it seems that set $HOME/.gitconfig user.name,user.email or
machine username and username@hostname.
2013-07-26 00:29:00 +09:00
takezoe
e15bd77789 (refs #2)Add forked count and repository tree view. 2013-07-25 20:47:35 +09:00
shimamoto
a5f12a50e6 Add view ISSUE_OUTLINE_VIEW. 2013-07-25 20:28:19 +09:00
takezoe
07ef06ad95 Improve authentication for H2 console. 2013-07-25 03:16:34 +09:00
takezoe
b61836adf7 Toggle Gravatar support at the system settings. 2013-07-25 03:00:46 +09:00
takezoe
34e2663492 Use JGitUtil.isEmpty() to check whether repository is empty. 2013-07-24 23:10:55 +09:00
Naoki Takezoe
8b90f87589 Merge pull request #51 from tanacasino/fix/search-in-empty-repository
(refs #50)Fix search logic in empty repository
2013-07-24 06:14:42 -07:00
takezoe
8c1e45da6c Set initial value of 'owner' parameter in the repository creation page. 2013-07-24 22:09:10 +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
Tomofumi Tanaka
62a6d74393 (refs #50)Fix search logic in empty repository 2013-07-24 16:46:46 +09:00
shimamoto
cb94447290 (refs #26) Add Dashboard controller. Uses a common design at issue. 2013-07-24 14:10:17 +09:00
shimamoto
e4cf509d0f Add tab at dashboard. 2013-07-24 14:01:10 +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
3a7391fbb3 (refs #8)Some fix for group management. 2013-07-24 03:36:42 +09:00
takezoe
2155734e23 (refs #8)Add Members tab to account information page for group account. 2013-07-24 02:12:35 +09:00
takezoe
6806e66d64 (refs #8)Change create/edit user template name and path. 2013-07-24 02:04:08 +09:00
takezoe
db8305b5e9 (refs #8)Change create/edit user template name and path. 2013-07-24 02:03:42 +09:00
takezoe
e8330eedc3 (refs #8)Group repository creation is completed. 2013-07-24 02:00:52 +09:00
takezoe
c01c4a860c (refs #8)Set initial value for editing group. 2013-07-24 01:54:33 +09:00
takezoe
6e778f209d (refs #8)Fix error message position. 2013-07-24 01:42:40 +09:00
takezoe
b760361184 (refs #8)Implementing repository creation for group. 2013-07-23 22:05:30 +09:00
takezoe
7150befa54 (refs #8)Group register/edit form is completed. 2013-07-23 18:52:36 +09:00
takezoe
5bf0b275cb (refs #8)Remove unused code. 2013-07-23 15:39:47 +09:00
takezoe
c86bf1d68b (refs #8)Merge user name proposal API to IndexController. 2013-07-23 15:37:59 +09:00
takezoe
e61bde1415 (refs #8)Implementing group register/edit form. 2013-07-23 13:02:30 +09:00
takezoe
e4b3f0ddef (refs #8)Implementing group register/edit form. 2013-07-23 11:59:49 +09:00
takezoe
ec73294900 (refs #8)Add model for GROUP_MEMBER. 2013-07-23 11:08:36 +09:00
takezoe
30eb949ce1 (refs #8)Start to implement group management. 2013-07-22 22:22:49 +09:00
takezoe
f5d69a3df6 Merge branch 'master' into group-management 2013-07-22 21:22:36 +09:00
takezoe
3cc39489bd (refs #40)Enable H2 Console. 2013-07-22 21:12:22 +09:00
takezoe
ace5d7de9e (refs #3)Separate search actions to SearchController. 2013-07-22 17:28:13 +09:00
takezoe
1682eb3915 (refs #8)Add DDL to add new table and columns for group management. 2013-07-22 17:15:12 +09:00
takezoe
6fd1a990ae (refs #44)Add milestone progress bar to the issue detail page. 2013-07-22 17:01:00 +09:00
Naoki Takezoe
cfa36a21b5 Merge pull request #47 from tomykaira/set_icon_on_assignee_change
Set icon when assignee is changed in page (via JS)
2013-07-21 20:29:09 -07:00
takezoe
95163d4864 Add link to the account info page for the assigned user icon at the issue list. 2013-07-22 12:27:18 +09:00
takezoe
5a9645829d (refs #33)Small fix for pull request #45. 2013-07-22 12:25:28 +09:00
takezoe
be78d93c1f Fix avatar tooltip. 2013-07-22 12:22:18 +09:00
Naoki Takezoe
ac63558645 Merge pull request #45 from tomykaira/feature/participants
Add participants to issue detail
2013-07-21 20:10:57 -07:00
tomykaira
88fb2e49dc Set icon when assignee is changed in page
The javascript code did not set icon, whereas the view script does.
2013-07-21 20:06:24 +09:00
tomykaira
6e96ad0f17 (refs #37)Add participants to issue detail
The design is inherited from Github.
2013-07-21 18:25:02 +09:00
takezoe
e54754d04f (refs #25)Display due date in milestone dropdown chooser. 2013-07-21 01:39:38 +09:00
takezoe
e4b2ebe2a4 (refs #25)Alert if due date passed. 2013-07-20 19:34:58 +09:00
takezoe
0028431dde Exclude some actions from comment count at the repository search result. 2013-07-20 03:06:33 +09:00
takezoe
91d94de1d2 Merge branch '#3_repository-search'
Conflicts:
	src/main/scala/app/UserManagementController.scala
	src/main/scala/service/IssuesService.scala
	src/main/twirl/issues/issue.scala.html
2013-07-20 03:00:16 +09:00
takezoe
0c131ec990 Move FileUploadUtil to FileUploadControllerBase. 2013-07-19 20:33:40 +09:00
takezoe
54280d5572 Add paginator and separate search code in controller to service. 2013-07-19 20:24:31 +09:00
Naoki Takezoe
6d3640a8b0 Merge pull request #43 from rabitarochan/fix/wiki-resourceleak
Fix resource leak.
2013-07-19 04:23:57 -07:00
rabitarochan
8226073506 Fix resource leak. 2013-07-19 18:18:44 +09:00
takezoe
f4a5e18c69 Merge branch 'repository-search-cache' into #3_repository-search 2013-07-19 18:04:28 +09:00
takezoe
133af93548 Don't use cache library immediately. 2013-07-19 18:03:48 +09:00
takezoe
3546a5d392 Ignore error in activity timeline caused by invalid data. 2013-07-19 14:11:13 +09:00
takezoe
fb921e951e Merge branch 'master' of https://github.com/takezoe/gitbucket 2013-07-19 14:08:05 +09:00
shimamoto
22685d8e3b Add reference link to wiki. 2013-07-19 13:25:23 +09:00
takezoe
b2d050d136 Remove unused import statement. 2013-07-19 12:17:40 +09:00
takezoe
e3ff1dcd96 Check state of repository type radio button had not been applied. 2013-07-19 03:48:58 +09:00
takezoe
897890e1b4 (refs #42)Requires BASIC authentication for /info/refs?service=git-receive-pack. 2013-07-19 03:23:19 +09:00
Naoki Takezoe
2c95ea00e8 GitBucket 1.3 released. 2013-07-18 20:59:59 +09:00
takezoe
00d6ed7dbb Cleanup search field and global header styles. 2013-07-18 20:50:42 +09:00
shimamoto
b23133c79c Move Implicit && to model package object. 2013-07-18 20:21:24 +09:00
takezoe
eef2f26707 Change highlight color. 2013-07-18 20:12:08 +09:00
takezoe
7483ad1732 Pagination for repository search results. 2013-07-18 20:02:12 +09:00
shimamoto
188237db24 Replace String#format() with string interpolation. 2013-07-18 19:54:07 +09:00
takezoe
134624967b Store search results into singleton cache. 2013-07-18 19:09:21 +09:00
takezoe
a1b8d1cd84 Add Guava to use CacheBuilder. 2013-07-18 19:08:03 +09:00
takezoe
e7b9293f3b Merge branch 'master' into #3_repository-search 2013-07-18 17:07:22 +09:00
takezoe
93e4a8931d (refs #3)Add search form to all repository related pages. 2013-07-18 17:06:28 +09:00
takezoe
c0713eaeda (refs #33)Use RequestCache instead of AccountService directly. 2013-07-18 15:55:59 +09:00
takezoe
000afa1ed6 Merge branch 'master' into #33_match-by-email
Conflicts:
	src/main/scala/util/JGitUtil.scala
	src/main/scala/view/helpers.scala
	src/main/twirl/repo/blob.scala.html
	src/main/twirl/repo/commit.scala.html
	src/main/twirl/repo/commits.scala.html
	src/main/twirl/repo/files.scala.html
2013-07-18 15:49:56 +09:00
takezoe
323e25951f Use Gravatar if committer is not registered in GitBucket. 2013-07-18 13:40:21 +09:00
Naoki Takezoe
49d0c0de87 Update for 1.3 release. 2013-07-18 12:46:08 +09:00
takezoe
e31a835c4e (refs #30)Add LICENSE file 2013-07-18 12:34:06 +09:00
takezoe
dedf5094c1 Small fix and add TODO. 2013-07-18 03:58:39 +09:00
takezoe
fed4619a92 Merge remote-tracking branch 'origin/master' 2013-07-18 03:53:22 +09:00
takezoe
9eb1a20b3f Batch updating for issues did not work with IE (also IE10).
I applied quick fix to release 1.3. Please update after 1.3 release if you have better solution.
2013-07-18 03:52:46 +09:00
Naoki Takezoe
ac21b9cc20 Update README.md 2013-07-18 03:35:08 +09:00
Naoki Takezoe
4a486e3bf8 Update README.md 2013-07-18 03:34:30 +09:00
takezoe
269374e6bb Quote src attribute of avatar image. 2013-07-18 03:11:56 +09:00
takezoe
e4d97e4059 (refs #3)Hide result count if count is zero. 2013-07-18 01:34:06 +09:00
takezoe
ba567d81cb (refs #3)Add result count to the menu. 2013-07-18 01:12:12 +09:00
takezoe
4fb6005f44 (refs #3)Apply likeEncode to search keyword. 2013-07-18 00:47:55 +09:00
takezoe
69ec4175eb (refs #3)Fix issue search condition. 2013-07-17 21:18:09 +09:00
takezoe
d46e90dcdb (refs #3)Improve presentation for code search results. 2013-07-17 21:13:53 +09:00
takezoe
900e91e101 Bugfix 2013-07-17 19:00:35 +09:00
takezoe
05d7e33d86 (refs #3)Add search form at the top of search result. 2013-07-17 18:32:59 +09:00
takezoe
7f0aff8c03 (refs #3)Cleanup 2013-07-17 16:59:38 +09:00
takezoe
512e59193d (refs #3)Issue search is temporary available. 2013-07-17 16:47:43 +09:00
shimamoto
8f056e4a82 Finished the issues controller refactoring. 2013-07-17 15:36:05 +09:00
takezoe
d06a986293 Remove unused import statement. 2013-07-17 13:57:45 +09:00
shimamoto
d866847c0d (refs #11) Fix comments to display. 2013-07-17 13:56:54 +09:00
takezoe
83472bc354 Remove unused import statement. 2013-07-17 13:55:26 +09:00
shimamoto
ac784f8905 (refs #11) Change the value to be set in the action. 2013-07-17 12:40:23 +09:00
shimamoto
6035281ca1 Change action(IssueComment) to String type. 2013-07-17 12:37:48 +09:00
takezoe
ce8168d97a Fix typo. 2013-07-17 11:54:10 +09:00
takezoe
27670525a3 (refs #3)Search by AND if query words are separated by whitespace. 2013-07-17 11:52:28 +09:00
takezoe
4796d7f450 (refs #3)Search git repository without cloning to the file system. 2013-07-17 06:36:21 +09:00
takezoe
79ec96343f (refs #3)Start work for repository search. 2013-07-17 03:24:47 +09:00
takezoe
4572a455c8 Change ISSUE_COMMENT.ACTION to NOT NULL. 2013-07-16 23:05:45 +09:00
takezoe
cb591925ea (refs #3)Add search field to header area. 2013-07-16 21:58:09 +09:00
takezoe
0bc6102096 (refs #39)Remove unnecessary attribute. 2013-07-16 21:31:42 +09:00
takezoe
3282a8d76a (refs #39)Small fix for copy button. 2013-07-16 21:30:21 +09:00
Naoki Takezoe
d04befb8d0 Merge pull request #39 from tanacasino/feature/copy-to-clipboard
Add copy to clipboard git clone URL
2013-07-16 05:04:47 -07: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
Tomofumi Tanaka
fc7481c60c Add copy to clipboard clone URL 2013-07-16 01:10:52 +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
4e8c130cbf Expand column COMMENT.ACTION to VARCHAR(20). 2013-07-12 16:07:20 +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
f34f60b255 Returns a gray image if username has not been registered. 2013-07-12 15:43:23 +09:00
takezoe
3c2675fd0d Remove unused import statement. 2013-07-12 15:23:01 +09:00
takezoe
f163e348e0 (refs #34)Link conversion checks existence of accounts and issues. 2013-07-12 15:15:58 +09:00
takezoe
71a3d79c82 Fix commit list presentation. 2013-07-12 04:34:54 +09:00
takezoe
bd1ba67647 Add avatar to the blob view. 2013-07-12 04:29:27 +09:00
takezoe
828688ddd0 (refs #33)Match committer by mail address. 2013-07-12 04:27:20 +09:00
takezoe
60cd1320d2 Migration for wiki repository configuration in 1.3 which add http.receivepack=true. 2013-07-12 03:30:25 +09:00
takezoe
a31de89f9c Remove debug code. 2013-07-12 03:26:41 +09:00
takezoe
a129f53e0c Remove fixed TODO. 2013-07-12 03:01:28 +09:00
takezoe
6aa86ac2e3 Use StringUtil#urlEncode() instead of URLEncode#encode(). 2013-07-12 02:18:29 +09:00
takezoe
28cafbcad2 (refs #35)Fixed. 2013-07-12 02:14:27 +09:00
takezoe
991f60ce44 (refs #34)@xxxx in markdown as link. 2013-07-12 01:29:23 +09:00
takezoe
5dbeabcc58 Support formaction attribute. 2013-07-11 22:19:03 +09:00
takezoe
b6bcebc588 Fix activity message. 2013-07-11 22:13:53 +09:00
shimamoto
2f3aa57d23 Merge branch 'master' of https://github.com/takezoe/gitbucket.git 2013-07-11 22:02:44 +09:00
shimamoto
a123774bab (refs #11) When the issue is closed or reopened, the comment id not
required.
2013-07-11 22:02:12 +09:00
takezoe
7f56b50267 Remove unnecessary code. 2013-07-11 21:26:27 +09:00
takezoe
386f0dc142 (refs #36)Handle unresolved revision string. 2013-07-11 21:24:09 +09:00
takezoe
bf90811cef Replace String#format() with string interpolation. 2013-07-11 20:22:45 +09:00
takezoe
72e2c6dca7 Replace String#format() with string interpolation. 2013-07-11 20:19:11 +09:00
takezoe
81fe467b20 Improve Git repository creation. 2013-07-11 19:47:48 +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
shimamoto
d59e358caa (refs #11) Add permission to html. 2013-07-11 15:03:57 +09:00
takezoe
5e1eb39b87 (refs #2)Experimental implementation of forking repository. 2013-07-11 14:39:25 +09:00
shimamoto
063170463f (refs #11) Add permission to html. 2013-07-11 14:01:09 +09:00
shimamoto
f8a9851bb3 Merge branch 'master' of https://github.com/takezoe/gitbucket.git 2013-07-11 13:44:12 +09:00
shimamoto
b81a30ef12 (refs #11) Implemented the batch update. 2013-07-11 13:43:42 +09:00
takezoe
62fb968c9a Fix showing branch if specified branch, tag or id does not exist. 2013-07-11 13:07:50 +09:00
takezoe
88b8567d2b Hide branch pulldown at Tags tab. 2013-07-11 12:50:05 +09:00
takezoe
7e4a295ef0 (refs #28)Add avatar icon to the issue detail page. 2013-07-11 12:26:37 +09:00
takezoe
289ed85365 Fix height of avatar icon. 2013-07-11 12:06:06 +09:00
takezoe
07dd459f3c Add method for request cache to app.Context. 2013-07-11 11:20:56 +09:00
takezoe
f104fab593 Rename StringUtil#encrypt() to sha1(). 2013-07-11 11:09:30 +09:00
takezoe
0170f9b44a Replace implicit conversion with implicit class. 2013-07-11 11:03:59 +09:00
takezoe
585d96949b Fix typo. 2013-07-11 11:02:48 +09:00
takezoe
46b3807f21 Fix redirect path after sign in. 2013-07-11 04:08:06 +09:00
takezoe
5e8a73e29d Small fix. 2013-07-11 04:07:42 +09:00
takezoe
796a276b65 (refs #28)Look up Gravatar if user icon is not configured. 2013-07-11 03:51:50 +09:00
takezoe
072290e544 Fix header and sign-in form presentation. 2013-07-11 03:50:00 +09:00
takezoe
5d5b642fa9 Fix avatar image style. 2013-07-11 00:49:03 +09:00
takezoe
70761f4ac1 (refs #27)Display assigned user on issue list. 2013-07-10 21:09:21 +09:00
takezoe
9d61f73e22 Generalize some methods in AccountController and UserManagementController. 2013-07-10 20:44:28 +09:00
takezoe
7a3c61a8d0 Generalize some methods in AccountController and UserManagementController. 2013-07-10 20:38:24 +09:00
takezoe
96872d7d41 (refs #28)Upload avatar part is separated from account editing form. 2013-07-10 20:20:05 +09:00
takezoe
485d6131d5 (refs #28)Display avatar images in some places. 2013-07-10 19:57:59 +09:00
takezoe
4893e9a58a (refs #28)Remove debug code. 2013-07-10 18:26:09 +09:00
takezoe
79480c1d73 (refs #28)Remove unnecessary import statement. 2013-07-10 18:25:24 +09:00
takezoe
02c015574f (refs #28)Fix information message. 2013-07-10 18:24:46 +09:00
takezoe
653872df8e (refs #28)Add SessionCleanupListener. 2013-07-10 18:23:56 +09:00
takezoe
248079f041 (refs #28)Display avatar icon on the activity timeline. 2013-07-10 14:37:00 +09:00
takezoe
2da756692b (refs #28)Avatar image can be uploaded at the account editing page. 2013-07-10 14:15:56 +09:00
shimamoto
240a749b87 (refs #11) Add button for batch update. 2013-07-10 12:13:19 +09:00
takezoe
e4324258d3 (refs #28)Implementing avatar image uploading. 2013-07-10 11:34:36 +09:00
takezoe
2c33abe5d1 (refs #28)Implementing avatar image uploading. 2013-07-10 03:01:46 +09:00
takezoe
09ef1e0319 (refs #28)Add Dropzone.js for Ajax based file uploading. 2013-07-10 03:01:14 +09:00
takezoe
c091d96999 Adjust div.box-header style. 2013-07-10 00:20:23 +09:00
takezoe
1978061a06 Display the message after settings updating is completed. 2013-07-10 00:16:55 +09:00
takezoe
617370e822 Rename SettingsController to RepositorySettingsController. 2013-07-10 00:09:30 +09:00
takezoe
0ed6a96781 Display the message after settings updating is completed. 2013-07-09 21:33:46 +09:00
takezoe
b3c3bf51ba Small fix. 2013-07-09 21:29:29 +09:00
takezoe
0e187fe888 Display last 3 commits for push action in the activity timeline. 2013-07-09 20:04:48 +09:00
takezoe
43efcf3a99 Adjust error message positions of sign-in form. 2013-07-09 19:58:39 +09:00
takezoe
ebc858aed9 (refs #31)Make it possible to create empty repository. 2013-07-09 19:41:00 +09:00
takezoe
f94af86ff9 Merge remote-tracking branch 'origin/master' 2013-07-09 15:45:49 +09:00
takezoe
da1c58bac6 Remove commit log before repository. 2013-07-09 15:44:45 +09:00
takezoe
c1c136f6c0 Remove activities before repository. 2013-07-09 13:18:13 +09:00
Naoki Takezoe
8a18119b53 Update README.md for 1.2 release. 2013-07-09 11:18:38 +09:00
201 changed files with 12377 additions and 2586 deletions

1
.gitignore vendored
View File

@@ -14,6 +14,7 @@ project/plugins/project/
.classpath
.project
.cache
.settings
# IntelliJ specific
.idea/

202
LICENSE Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

121
README.md
View File

@@ -1,25 +1,30 @@
GitBucket
=========
GitBucket is a Github clone by Scala, Easy to setup.
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
- Timeline
- 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).
@@ -27,19 +32,109 @@ Installation
--------
1. Download latest **gitbucket.war** from [the release page](https://github.com/takezoe/gitbucket/releases).
2. Deploy it to the servlet container such as Tomcat or Jetty.
2. Deploy it to the Servlet 3.0 container such as Tomcat 7.x, Jetty 8.x, GlassFish 3.x or higher.
3. Access **http://[hostname]:[port]/gitbucket/** using your web browser.
The default administrator account is **root** and password is **root**.
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]
- --host=[HOSTNAME]
- --https=true
- --gitbucket.home=[DATA_DIR]
To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk.
For Installation on Windows Server with IIS see [this wiki page](https://github.com/takezoe/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo)
### Mac OS X
On OS X, copy the [gitbucket.plist](https://raw.github.com/takezoe/gitbucket/master/contrib/macosx/gitbucket.plist) file to `~/Library/LaunchAgents/`
Run the following commands in `Terminal` to
- start gitbucket: `launchctl load ~/Library/LaunchAgents/gitbucket.plist`
- stop gitbucket: `launchctl unload ~/Library/LaunchAgents/gitbucket.plist`
Release Notes
--------
### 1.9 - 28 Dec 2013
- Display GITBUCKET_HOME on the system settings page
- Fix some bugs
### 1.8 - 30 Nov 2013
- Add user and group deletion
- Improve pull request performance
- Pull request synchronization (when source repository is updated after pull request, it's applied to the pull request)
- LDAP StartTLS support
- Enable hard wrapping in Markdown
- Add new some options to specify the data directory. See details in [Wiki](https://github.com/takezoe/gitbucket/wiki/DirectoryStructure).
- Fix some bugs
### 1.7 - 26 Oct 2013
- Support working on Java6 in embedded Jetty mode
- Add ```--host``` option to bind specified host name in embedded Jetty mode
- Add ```--https=true``` option to force https scheme when using embedded Jetty mode at the back of https proxy
- Add full name as user property
- Change link color for absent Wiki pages
- Add ZIP download button to the repository viewer tab
- Improve ZIP exporting performance
- Expand issue and comment textarea for long text automatically
- Add conflict detection in Wiki
- Add reverting wiki page from history
- Match committer to user name by email address
- Mail notification sender is customizable
- Add link to changeset in refs comment for issues
- Fix some bugs
### 1.6 - 1 Oct 2013
- Web hook
- Performance improvement for pull request
- Executable war file
- Specify suitable Content-Type for downloaded files in the repository viewer
- Fix 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
- Fix some bugs
### 1.4 - 31 Jul 2013
- Group management
- Repository search for code and issues
- Display user related issues on the dashboard
- Display participants avatar of issues on the issue page
- Performance improvement for repository viewer
- Alert by milestone due date
- H2 database administration console
- Fix some bugs
### 1.3 - 18 Jul 2013
- Batch updating for issues
- Display assigned user on issue list
- User icon and Gravatar support
- Convert @xxxx to link to the account page
- Add copy to clipboard button for git clone URL
- Allow multi-byte characters as wiki page name
- Allow to create the empty repository
- Fix some bugs
### 1.2 - 09 Jul 2013
- Add activity timeline
- Bugfix for Git 1.8.1.5 or later
- Allow multi-byte characters as label
- Fix some bugs
### 1.1 - 05 Jul 2013
- Fixed some bugs.
- Upgrade to JGit 3.0.
- Fix some bugs
- Upgrade to JGit 3.0
### 1.0 - 04 Jul 2013
- This is a first public release.
- This is a first public release

61
build.xml Normal file
View File

@@ -0,0 +1,61 @@
<?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"/>
<condition property="sbt.exec" value="sbt.bat" else="sbt.sh">
<os family="windows" />
</condition>
<target name="clean">
<delete dir="${embed.classes.dir}"/>
<delete file="${target.dir}/scala-${scala.version}/gitbucket.war"/>
</target>
<target name="war" depends="clean">
<exec executable="${sbt.exec}" resolveexecutable="true" failonerror="true">
<arg line="clean compile test package" />
</exec>
</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,HttpsSupportConnector.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>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>gitbucket</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/java</string>
<string>-Dmail.smtp.starttls.enable=true</string>
<string>-jar</string>
<string>gitbucket.war</string>
<string>--host=127.0.0.1</string>
<string>--port=8080</string>
<string>--https=true</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>

View File

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

View File

@@ -0,0 +1,109 @@
#!/bin/bash
#
# /etc/rc.d/init.d/gitbucket
#
# Starts the GitBucket server
#
# chkconfig: 345 60 40
# description: Run GitBucket server
# processname: java
# Source function library
. /etc/rc.d/init.d/functions
# Default values
GITBUCKET_HOME=/var/lib/gitbucket
GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
# Pull in cq settings
[ -f /etc/sysconfig/gitbucket ] && . /etc/sysconfig/gitbucket
# Location of the log and PID file
LOG_FILE=/var/log/gitbucket/run.log
PID_FILE=/var/run/gitbucket.pid
# Default return value
RETVAL=0
start() {
echo -n $"Starting GitBucket server: "
# Compile statup parameters
if [ $GITBUCKET_PORT ]; then
START_OPTS="${START_OPTS} --port=${GITBUCKET_PORT}"
fi
if [ $GITBUCKET_PREFIX ]; then
START_OPTS="${START_OPTS} --prefix=${GITBUCKET_PREFIX}"
fi
if [ $GITBUCKET_HOST ]; then
START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
fi
if [ $GITBUCKET_HTTPS ]; then
START_OPTS="${START_OPTS} --https=true"
fi
# Run the Java process
GITBUCKET_HOME="${GITBUCKET_HOME}" java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 &
RETVAL=$?
# Store PID of the Java process into a file
echo $! > $PID_FILE
if [ $RETVAL -eq 0 ] ; then
success "GitBucket startup"
else
failure "GitBucket startup"
fi
echo
return $RETVAL
}
stop() {
echo -n $"Stopping GitBucket server: "
# Run the Java process
kill $(cat $PID_FILE 2>/dev/null) >>$LOG_FILE 2>&1
RETVAL=$?
if [ $RETVAL -eq 0 ] ; then
rm -f $PID_FILE
success "GitBucket stopping"
else
failure "GitBucket stopping"
fi
echo
return $RETVAL
}
restart() {
stop
start
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
status)
status -p $PID_FILE java
RETVAL=$?
;;
*)
echo $"Usage: $0 [start|stop|restart|status]"
RETVAL=2
esac
exit $RETVAL

View File

@@ -0,0 +1,47 @@
Name: gitbucket
Summary: GitHub clone written with Scala.
Version: 1.7
Release: 1%{?dist}
License: Apache
URL: https://github.com/takezoe/gitbucket
Group: System/Servers
Source0: %{name}.war
Source1: %{name}.init
Source2: %{name}.conf
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
BuildArch: noarch
Requires: java >= 1.7
%description
GitBucket is the easily installable GitHub clone written with Scala.
%install
[ "%{buildroot}" != / ] && %{__rm} -rf "%{buildroot}"
%{__mkdir_p} %{buildroot}{%{_sysconfdir}/{init.d,sysconfig},%{_datarootdir}/%{name}/lib,%{_sharedstatedir}/%{name},%{_localstatedir}/log/%{name}}
%{__install} -m 0644 %{SOURCE0} %{buildroot}%{_datarootdir}/%{name}/lib
%{__install} -m 0755 %{SOURCE1} %{buildroot}%{_sysconfdir}/init.d/%{name}
%{__install} -m 0644 %{SOURCE2} %{buildroot}%{_sysconfdir}/sysconfig/%{name}
touch %{buildroot}%{_localstatedir}/log/%{name}/run.log
%clean
[ "%{buildroot}" != / ] && %{__rm} -rf "%{buildroot}"
%files
%defattr(-,root,root,-)
%{_datarootdir}/%{name}/lib/%{name}.war
%{_sysconfdir}/init.d/%{name}
%config %{_sysconfdir}/sysconfig/%{name}
%{_localstatedir}/log/%{name}/run.log
%changelog
* Mon Oct 28 2013 Jiri Tyr <jiri_DOT_tyr at gmail.com>
- Version bump to v1.7.
* Thu Oct 17 2013 Jiri Tyr <jiri_DOT_tyr at gmail.com>
- First build.

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.

1
project/build.properties Normal file
View File

@@ -0,0 +1 @@
sbt.version=0.12.3

View File

@@ -1,7 +1,6 @@
import sbt._
import Keys._
import org.scalatra.sbt._
import org.scalatra.sbt.PluginKeys._
import twirl.sbt.TwirlPlugin._
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
@@ -9,8 +8,8 @@ object MyBuild extends Build {
val Organization = "jp.sf.amateras"
val Name = "gitbucket"
val Version = "0.0.1"
val ScalaVersion = "2.10.1"
val ScalatraVersion = "2.2.0"
val ScalaVersion = "2.10.3"
val ScalatraVersion = "2.2.1"
lazy val project = Project (
"gitbucket",
@@ -20,24 +19,35 @@ 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/"
),
scalacOptions := Seq("-deprecation", "-language:postfixOps"),
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",
"org.json4s" %% "json4s-jackson" % "3.2.5",
"jp.sf.amateras" %% "scalatra-forms" % "0.0.11",
"commons-io" % "commons-io" % "2.4",
"org.pegdown" % "pegdown" % "1.3.0",
"org.pegdown" % "pegdown" % "1.4.1",
"org.apache.commons" % "commons-compress" % "1.5",
"org.apache.commons" % "commons-email" % "1.3.1",
"org.apache.httpcomponents" % "httpclient" % "4.3",
"com.typesafe.slick" %% "slick" % "1.0.1",
"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.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar"))
"com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.3.173",
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"),
"junit" % "junit" % "4.11" % "test"
),
EclipseKeys.withSource := true
EclipseKeys.withSource := true,
javacOptions in compile ++= Seq("-target", "6", "-source", "6"),
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),
packageOptions += Package.MainClass("JettyLauncher")
) ++ seq(Twirl.settings: _*)
)
}
}

View File

@@ -1,7 +1,9 @@
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")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.2")

View File

@@ -1,2 +1,2 @@
set SCRIPT_DIR=%~dp0
java -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar "%SCRIPT_DIR%\sbt-launch-0.12.3.jar" %*
java -Dsbt.log.noformat=true -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 -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar `dirname $0`/sbt-launch-0.12.3.jar "$@"

View File

@@ -0,0 +1,77 @@
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.webapp.WebAppContext;
import java.io.IOException;
import java.net.URL;
import java.security.ProtectionDomain;
public class JettyLauncher {
public static void main(String[] args) throws Exception {
String host = null;
int port = 8080;
String contextPath = "/";
boolean forceHttps = false;
for(String arg: args) {
if(arg.startsWith("--") && arg.contains("=")) {
String[] dim = arg.split("=");
if(dim.length >= 2) {
if(dim[0].equals("--host")) {
host = dim[1];
} else if(dim[0].equals("--port")) {
port = Integer.parseInt(dim[1]);
} else if(dim[0].equals("--prefix")) {
contextPath = dim[1];
} else if(dim[0].equals("--https") && (dim[1].equals("1") || dim[1].equals("true"))) {
forceHttps = true;
} else if(dim[0].equals("--gitbucket.home")){
System.setProperty("gitbucket.home", dim[1]);
}
}
}
}
Server server = new Server();
HttpsSupportConnector connector = new HttpsSupportConnector(forceHttps);
if(host != null) {
connector.setHost(host);
}
connector.setMaxIdleTime(1000 * 60 * 60);
connector.setSoLingerTime(-1);
connector.setPort(port);
server.addConnector(connector);
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();
}
}
class HttpsSupportConnector extends SelectChannelConnector {
private boolean forceHttps;
public HttpsSupportConnector(boolean forceHttps) {
this.forceHttps = forceHttps;
}
@Override
public void customize(final EndPoint endpoint, final Request request) throws IOException {
if (this.forceHttps) {
request.setScheme("https");
super.customize(endpoint, request);
}
}
}

View File

@@ -0,0 +1,93 @@
package util;
import org.eclipse.jgit.api.errors.PatchApplyException;
import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.patch.FileHeader;
import org.eclipse.jgit.patch.HunkHeader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
/**
* This class helps to apply patch. Most of these code came from {@link org.eclipse.jgit.api.ApplyCommand}.
*/
public class PatchUtil {
public static String apply(String source, String patch, FileHeader fh)
throws IOException, PatchApplyException {
RawText rt = new RawText(source.getBytes("UTF-8"));
List<String> oldLines = new ArrayList<String>(rt.size());
for (int i = 0; i < rt.size(); i++)
oldLines.add(rt.getString(i));
List<String> newLines = new ArrayList<String>(oldLines);
for (HunkHeader hh : fh.getHunks()) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(patch.getBytes("UTF-8"), hh.getStartOffset(), hh.getEndOffset() - hh.getStartOffset());
RawText hrt = new RawText(out.toByteArray());
List<String> hunkLines = new ArrayList<String>(hrt.size());
for (int i = 0; i < hrt.size(); i++)
hunkLines.add(hrt.getString(i));
int pos = 0;
for (int j = 1; j < hunkLines.size(); j++) {
String hunkLine = hunkLines.get(j);
switch (hunkLine.charAt(0)) {
case ' ':
if (!newLines.get(hh.getNewStartLine() - 1 + pos).equals(
hunkLine.substring(1))) {
throw new PatchApplyException(MessageFormat.format(
JGitText.get().patchApplyException, hh));
}
pos++;
break;
case '-':
if (!newLines.get(hh.getNewStartLine() - 1 + pos).equals(
hunkLine.substring(1))) {
throw new PatchApplyException(MessageFormat.format(
JGitText.get().patchApplyException, hh));
}
newLines.remove(hh.getNewStartLine() - 1 + pos);
break;
case '+':
newLines.add(hh.getNewStartLine() - 1 + pos,
hunkLine.substring(1));
pos++;
break;
}
}
}
if (!isNoNewlineAtEndOfFile(fh))
newLines.add(""); //$NON-NLS-1$
if (!rt.isMissingNewlineAtEnd())
oldLines.add(""); //$NON-NLS-1$
if (!isChanged(oldLines, newLines))
return null; // don't touch the file
StringBuilder sb = new StringBuilder();
for (String l : newLines) {
// don't bother handling line endings - if it was windows, the \r is
// still there!
sb.append(l).append('\n');
}
sb.deleteCharAt(sb.length() - 1);
return sb.toString();
}
private static boolean isChanged(List<String> ol, List<String> nl) {
if (ol.size() != nl.size())
return true;
for (int i = 0; i < ol.size(); i++)
if (!ol.get(i).equals(nl.get(i)))
return true;
return false;
}
private static boolean isNoNewlineAtEndOfFile(FileHeader fh) {
HunkHeader lastHunk = fh.getHunks().get(fh.getHunks().size() - 1);
RawText lhrt = new RawText(lastHunk.getBuffer());
return lhrt.getString(lhrt.size() - 1).equals(
"\\ No newline at end of file"); //$NON-NLS-1$
}
}

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.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,8 @@
ALTER TABLE ACCOUNT ADD COLUMN IMAGE VARCHAR(100);
UPDATE ISSUE_COMMENT SET ACTION = 'comment' WHERE ACTION IS NULL;
ALTER TABLE ISSUE_COMMENT ALTER COLUMN ACTION VARCHAR(20) NOT NULL;
UPDATE ISSUE_COMMENT SET ACTION = 'close_comment' WHERE ACTION = 'close';
UPDATE ISSUE_COMMENT SET ACTION = 'reopen_comment' WHERE ACTION = 'reopen';

View File

@@ -0,0 +1,24 @@
CREATE TABLE GROUP_MEMBER(
GROUP_NAME VARCHAR(100) NOT NULL,
USER_NAME VARCHAR(100) NOT NULL
);
ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_PK PRIMARY KEY (GROUP_NAME, USER_NAME);
ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_FK0 FOREIGN KEY (GROUP_NAME) REFERENCES ACCOUNT (USER_NAME);
ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_FK1 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);
ALTER TABLE ACCOUNT ADD COLUMN GROUP_ACCOUNT BOOLEAN NOT NULL DEFAULT FALSE;
CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS
SELECT
A.USER_NAME,
A.REPOSITORY_NAME,
A.ISSUE_ID,
NVL(B.COMMENT_COUNT, 0) AS COMMENT_COUNT
FROM ISSUE A
LEFT OUTER JOIN (
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT
WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment')
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
) B
ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID);

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

@@ -0,0 +1,5 @@
ALTER TABLE ACCOUNT ADD COLUMN FULL_NAME VARCHAR(100);
UPDATE ACCOUNT SET FULL_NAME = USER_NAME WHERE FULL_NAME IS NULL;
ALTER TABLE ACCOUNT ALTER COLUMN FULL_NAME SET NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE ACCOUNT ADD COLUMN REMOVED BOOLEAN DEFAULT FALSE;

View File

@@ -1,11 +1,23 @@
import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter}
import app._
import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
import org.scalatra._
import javax.servlet._
import java.util.EnumSet
class ScalatraBootstrap extends LifeCycle {
override def init(context: ServletContext) {
// Register TransactionFilter and BasicAuthenticationFilter at first
context.addFilter("transactionFilter", new TransactionFilter)
context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter)
context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
// Register controllers
context.mount(new IndexController, "/")
context.mount(new SignInController, "/*")
context.mount(new SearchController, "/")
context.mount(new FileUploadController, "/upload")
context.mount(new DashboardController, "/*")
context.mount(new UserManagementController, "/*")
context.mount(new SystemSettingsController, "/*")
context.mount(new CreateRepositoryController, "/*")
@@ -15,8 +27,11 @@ class ScalatraBootstrap extends LifeCycle {
context.mount(new LabelsController, "/*")
context.mount(new MilestonesController, "/*")
context.mount(new IssuesController, "/*")
context.mount(new SettingsController, "/*")
context.mount(new PullRequestsController, "/*")
context.mount(new RepositorySettingsController, "/*")
context.mount(new ValidationJavaScriptProvider, "/assets/common/js/*")
// Create GITBUCKET_HOME directory if it does not exist
val dir = new java.io.File(_root_.util.Directory.GitBucketHome)
if(!dir.exists){
dir.mkdirs()

View File

@@ -1,33 +1,43 @@
package app
import service._
import util.OneselfAuthenticator
import util.{FileUtil, OneselfAuthenticator}
import util.StringUtil._
import util.Directory._
import jp.sf.amateras.scalatra.forms._
import org.scalatra.FlashMapSupport
import org.apache.commons.io.FileUtils
class AccountController extends AccountControllerBase
with SystemSettingsService with AccountService with RepositoryService with ActivityService
with OneselfAuthenticator
trait AccountControllerBase extends ControllerBase {
trait AccountControllerBase extends AccountManagementControllerBase with FlashMapSupport {
self: SystemSettingsService with AccountService with RepositoryService with ActivityService
with OneselfAuthenticator =>
case class AccountNewForm(userName: String, password: String,mailAddress: String, url: Option[String])
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String])
case class AccountEditForm(password: Option[String], mailAddress: String, url: Option[String])
case class AccountEditForm(password: Option[String], fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String], clearImage: Boolean)
val newForm = mapping(
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" , text(required, maxlength(20)))),
"fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
"url" -> trim(label("URL" , optional(text(maxlength(200)))))
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text())))
)(AccountNewForm.apply)
val editForm = mapping(
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
"fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
"url" -> trim(label("URL" , optional(text(maxlength(200)))))
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))),
"clearImage" -> trim(label("Clear image" , boolean()))
)(AccountEditForm.apply)
/**
@@ -35,57 +45,96 @@ trait AccountControllerBase extends ControllerBase {
*/
get("/:userName") {
val userName = params("userName")
getAccountByUserName(userName).map { x =>
getAccountByUserName(userName).map { account =>
params.getOrElse("tab", "repositories") match {
// Public Activity
case "activity" => account.html.activity(x, getActivitiesByUser(userName, true))
case "activity" =>
_root_.account.html.activity(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
getActivitiesByUser(userName, true))
// Members
case "members" if(account.isGroupAccount) =>
_root_.account.html.members(account, getGroupMembers(account.userName))
// Repositories
case _ => account.html.repositories(x, getVisibleRepositories(userName, baseUrl, context.loginAccount.map(_.userName)))
case _ =>
_root_.account.html.repositories(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
getVisibleRepositories(context.loginAccount, baseUrl, Some(userName)))
}
} getOrElse NotFound
}
get("/:userName/_avatar"){
val userName = params("userName")
getAccountByUserName(userName).flatMap(_.image).map { image =>
contentType = FileUtil.getMimeType(image)
new java.io.File(getUserUploadDir(userName), image)
} getOrElse {
contentType = "image/png"
Thread.currentThread.getContextClassLoader.getResourceAsStream("noimage.png")
}
}
get("/:userName/_edit")(oneselfOnly {
val userName = params("userName")
getAccountByUserName(userName).map(x => account.html.edit(Some(x))) getOrElse NotFound
getAccountByUserName(userName).map(x => account.html.edit(Some(x), flash.get("info"))) getOrElse NotFound
})
post("/:userName/_edit", editForm)(oneselfOnly { form =>
val userName = params("userName")
getAccountByUserName(userName).map { account =>
updateAccount(account.copy(
password = form.password.map(encrypt).getOrElse(account.password),
password = form.password.map(sha1).getOrElse(account.password),
fullName = form.fullName,
mailAddress = form.mailAddress,
url = form.url))
redirect("/%s".format(userName))
updateImage(userName, form.fileId, form.clearImage)
flash += "info" -> "Account information has been updated."
redirect(s"/${userName}/_edit")
} getOrElse NotFound
})
get("/:userName/_delete")(oneselfOnly {
val userName = params("userName")
getAccountByUserName(userName, true).foreach { account =>
// Remove repositories
getRepositoryNamesOfUser(userName).foreach { repositoryName =>
deleteRepository(userName, repositoryName)
FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
}
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
removeUserRelatedData(userName)
updateAccount(account.copy(isRemoved = true))
}
session.invalidate
redirect("/")
})
get("/register"){
if(loadSystemSettings().allowAccountRegistration){
account.html.edit(None)
if(context.loginAccount.isDefined){
redirect("/")
} else {
account.html.edit(None, None)
}
} else NotFound
}
post("/register", newForm){ newForm =>
post("/register", newForm){ form =>
if(loadSystemSettings().allowAccountRegistration){
createAccount(newForm.userName, encrypt(newForm.password), newForm.mailAddress, false, newForm.url)
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url)
updateImage(form.userName, form.fileId, false)
redirect("/signin")
} else NotFound
}
// TODO Merge with UserManagementController
private def uniqueUserName: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
getAccountByUserName(value).map { _ => "User already exists." }
}
// TODO Merge with UserManagementController
private def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
getAccountByMailAddress(value)
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
.map { _ => "Mail address is already registered." }
}
}

View File

@@ -1,90 +1,209 @@
package app
import model.Account
import util.Validations
import _root_.util.Directory._
import _root_.util.Implicits._
import _root_.util.ControlUtil._
import _root_.util.{StringUtil, FileUtil, Validations, Keys}
import org.scalatra._
import org.scalatra.json._
import org.json4s._
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import model.Account
import scala.Some
import service.AccountService
import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest}
import java.text.SimpleDateFormat
import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
import org.scalatra.i18n._
/**
* Provides generic features for ScalatraServlet implementations.
* Provides generic features for controller implementations.
*/
abstract class ControllerBase extends ScalatraFilter
with ClientSideValidationFormSupport with JacksonJsonSupport with Validations {
with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with Validations {
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]
val context = request.getServletContext.getContextPath
val path = httpRequest.getRequestURI.substring(context.length)
if(path.startsWith("/console/")){
val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
if(account == null){
// Redirect to login form
httpResponse.sendRedirect(context + "/signin?" + StringUtil.urlEncode(path))
} else if(account.isAdmin){
// H2 Console (administrators only)
chain.doFilter(request, response)
} else {
// Redirect to dashboard
httpResponse.sendRedirect(context + "/")
}
} else if(path.startsWith("/git/")){
// Git repository
chain.doFilter(request, response)
} else {
// Scalatra actions
super.doFilter(request, response, chain)
}
}
/**
* Returns the context object for the request.
*/
implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL)
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 : ValueType[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 : ValueType[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=" + StringUtil.urlEncode(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))
}
}
case class Context(path: String, loginAccount: Option[Account], currentUrl: String)
/**
* Context object for the current request.
*/
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 =
defining(Keys.Request.Cache(key)){ cacheKey =>
Option(request.getAttribute(cacheKey).asInstanceOf[A]).getOrElse {
val newObject = action
request.setAttribute(cacheKey, newObject)
newObject
}
}
}
/**
* Base trait for controllers which manages account information.
*/
trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase {
self: AccountService =>
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()
updateAvatarImage(userName, None)
}
} else {
fileId.map { fileId =>
val filename = "avatar." + FileUtil.getExtension(getUploadedFilename(fileId).get)
FileUtils.moveFile(
getTemporaryFile(fileId),
new java.io.File(getUserUploadDir(userName), filename)
)
updateAvatarImage(userName, Some(filename))
}
}
protected def uniqueUserName: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
getAccountByUserName(value, true).map { _ => "User already exists." }
}
protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
getAccountByMailAddress(value, true)
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
.map { _ => "Mail address is already registered." }
}
}
/**
* Base trait for controllers which needs file uploading feature.
*/
trait FileUploadControllerBase {
def generateFileId: String =
new SimpleDateFormat("yyyyMMddHHmmSSsss").format(new java.util.Date(System.currentTimeMillis))
def TemporaryDir(implicit session: HttpSession): java.io.File =
new java.io.File(GitBucketHome, s"tmp/_upload/${session.getId}")
def getTemporaryFile(fileId: String)(implicit session: HttpSession): java.io.File =
new java.io.File(TemporaryDir, fileId)
// def removeTemporaryFile(fileId: String)(implicit session: HttpSession): Unit =
// getTemporaryFile(fileId).delete()
def removeTemporaryFiles()(implicit session: HttpSession): Unit =
FileUtils.deleteDirectory(TemporaryDir)
def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] =
session.getAndRemove[String](Keys.Session.Upload(fileId))
}

View File

@@ -1,105 +1,199 @@
package app
import util.Directory._
import util.UsersAuthenticator
import util.ControlUtil._
import util._
import service._
import java.io.File
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib._
import org.apache.commons.io._
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.lib.{FileMode, Constants}
import org.eclipse.jgit.dircache.DirCache
import org.scalatra.i18n.Messages
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 WikiService with LabelsService with ActivityService
with UsersAuthenticator =>
self: RepositoryService with AccountService with WikiService with LabelsService with ActivityService
with UsersAuthenticator with ReadableUsersAuthenticator =>
case class RepositoryCreationForm(name: String, description: Option[String])
case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
val form = mapping(
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, unique))),
"description" -> trim(label("Description" , optional(text())))
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()))),
"isPrivate" -> trim(label("Repository Type", boolean())),
"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.
*/
get("/new")(usersOnly {
html.newrepo()
html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
})
/**
* Create new repository.
*/
post("/new", form)(usersOnly { form =>
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, loginUserName, form.description)
// Insert to the database at first
createRepository(form.name, form.owner, form.description, form.isPrivate)
// Insert default labels
createLabel(loginUserName, form.name, "bug", "fc2929")
createLabel(loginUserName, form.name, "duplicate", "cccccc")
createLabel(loginUserName, form.name, "enhancement", "84b6eb")
createLabel(loginUserName, form.name, "invalid", "e6e6e6")
createLabel(loginUserName, form.name, "question", "cc317c")
createLabel(loginUserName, form.name, "wontfix", "ffffff")
// Add collaborators for group repository
if(ownerAccount.isGroupAccount){
getGroupMembers(form.owner).foreach { userName =>
addCollaborator(form.owner, form.name, userName)
}
}
// Create the actual repository
val gitdir = getRepositoryDir(loginUserName, form.name)
val repository = new RepositoryBuilder().setGitDir(gitdir).setBare.build
// Insert default labels
insertDefaultLabels(form.owner, form.name)
repository.create
// Create the actual repository
val gitdir = getRepositoryDir(form.owner, form.name)
JGitUtil.initRepository(gitdir)
val config = repository.getConfig
config.setBoolean("http", null, "receivepack", true)
config.save
if(form.createReadme){
using(Git.open(gitdir)){ git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
val content = if(form.description.nonEmpty){
form.name + "\n" +
"===============\n" +
"\n" +
form.description.get
} else {
form.name + "\n" +
"===============\n"
}
val tmpdir = getInitRepositoryDir(loginUserName, 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.setMessage("Initial commit").call
git.push.call
} finally {
FileUtils.deleteDirectory(tmpdir)
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
}
}
// Create Wiki repository
createWikiRepository(loginAccount, form.owner, form.name)
// Record activity
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
}
// redirect to the repository
redirect(s"/${form.owner}/${form.name}")
}
// Create Wiki repository
createWikiRepository(loginAccount, form.name)
// Record activity
recordCreateRepositoryActivity(loginUserName, form.name, loginUserName)
// redirect to the repository
redirect("/%s/%s".format(loginUserName, 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(repository.owner == loginUserName){
// redirect to the repository
redirect(s"/${repository.owner}/${repository.name}")
} else {
getForkedRepositories(repository.owner, repository.name).find(_._1 == loginUserName).map { case (owner, name) =>
// redirect to the repository
redirect(s"/${owner}/${name}")
} getOrElse {
// 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"/${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(){
override def validate(name: String, value: String, messages: Messages): 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] =
getRepositoryNamesOfUser(context.loginAccount.get.userName).find(_ == value).map(_ => "Repository already exists.")
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
params.get("owner").flatMap { userName =>
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
}
}
}
}

View File

@@ -0,0 +1,109 @@
package app
import service._
import util.{UsersAuthenticator, Keys}
import util.Implicits._
class DashboardController extends DashboardControllerBase
with IssuesService with PullRequestService with RepositoryService with AccountService
with UsersAuthenticator
trait DashboardControllerBase extends ControllerBase {
self: IssuesService with PullRequestService with RepositoryService with UsersAuthenticator =>
get("/dashboard/issues/repos")(usersOnly {
searchIssues("all")
})
get("/dashboard/issues/assigned")(usersOnly {
searchIssues("assigned")
})
get("/dashboard/issues/created_by")(usersOnly {
searchIssues("created_by")
})
get("/dashboard/pulls")(usersOnly {
searchPullRequests("created_by", None)
})
get("/dashboard/pulls/owned")(usersOnly {
searchPullRequests("created_by", None)
})
get("/dashboard/pulls/public")(usersOnly {
searchPullRequests("not_created_by", None)
})
get("/dashboard/pulls/for/:owner/:repository")(usersOnly {
searchPullRequests("all", Some(params("owner") + "/" + params("repository")))
})
private def searchIssues(filter: String) = {
import IssuesService._
// condition
val condition = session.putAndGet(Keys.Session.DashboardIssues,
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition())
)
val userName = context.loginAccount.get.userName
val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request)
//
dashboard.html.issues(
issues.html.listparts(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, repositories: _*),
page,
countIssue(condition.copy(state = "open"), filterUser, false, repositories: _*),
countIssue(condition.copy(state = "closed"), filterUser, false, repositories: _*),
condition),
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: _*)
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

@@ -0,0 +1,32 @@
package app
import _root_.util.{Keys, FileUtil}
import util.ControlUtil._
import org.scalatra._
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport}
import org.apache.commons.io.FileUtils
/**
* Provides Ajax based file upload functionality.
*
* This servlet saves uploaded file as temporary file and returns the unique id.
* You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id.
*/
class FileUploadController extends ScalatraServlet
with FileUploadSupport with FlashMapSupport with FileUploadControllerBase {
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
post("/image"){
fileParams.get("file") match {
case Some(file) if(FileUtil.isImage(file.name)) => defining(generateFileId){ fileId =>
FileUtils.writeByteArrayToFile(getTemporaryFile(fileId), file.get)
session += Keys.Session.Upload(fileId) -> file.name
Ok(fileId)
}
case None => BadRequest
}
}
}

View File

@@ -1,18 +1,83 @@
package app
import util._
import util.Implicits._
import service._
import jp.sf.amateras.scalatra.forms._
class IndexController extends IndexControllerBase
with RepositoryService with AccountService with SystemSettingsService with ActivityService
with RepositoryService with SystemSettingsService with ActivityService with AccountService
with UsersAuthenticator
trait IndexControllerBase extends ControllerBase {
self: RepositoryService with SystemSettingsService with ActivityService with AccountService with UsersAuthenticator =>
case class SignInForm(userName: String, password: String)
val form = mapping(
"userName" -> trim(label("Username", text(required))),
"password" -> trim(label("Password", text(required)))
)(SignInForm.apply)
trait IndexControllerBase extends ControllerBase { self: RepositoryService
with SystemSettingsService with ActivityService =>
get("/"){
val loginAccount = context.loginAccount
html.index(getRecentActivities(),
getAccessibleRepositories(context.loginAccount, baseUrl),
getVisibleRepositories(loginAccount, baseUrl),
loadSystemSettings(),
context.loginAccount.map{ account => getRepositoryNamesOfUser(account.userName) }.getOrElse(Nil))
loginAccount.map{ account => getUserRepositories(account.userName, baseUrl) }.getOrElse(Nil)
)
}
}
get("/signin"){
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 =>
authenticate(loadSystemSettings(), form.userName, form.password) match {
case Some(account) => signin(account)
case None => redirect("/signin")
}
}
get("/signout"){
session.invalidate
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("/")
}
}
/**
* JSON API for collaborator completion.
*
* TODO Move to other controller?
*/
get("/_user/proposals")(usersOnly {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray)
)
})
}

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
@@ -12,15 +14,14 @@ class IssuesController extends IssuesControllerBase
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
trait IssuesControllerBase extends ControllerBase {
self: IssuesService with RepositoryService with LabelsService with MilestonesService with ActivityService
self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
case class IssueCreateForm(title: String, content: Option[String],
assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
case class IssueEditForm(title: String, content: Option[String])
case class CommentForm(issueId: Int, content: String)
case class IssueStateForm(issueId: Int, content: Option[String])
val issueCreateForm = mapping(
"title" -> trim(label("Title", text(required))),
@@ -40,6 +41,11 @@ trait IssuesControllerBase extends ControllerBase {
"content" -> trim(label("Comment", text(required)))
)(CommentForm.apply)
val issueStateForm = mapping(
"issueId" -> label("Issue Id", number()),
"content" -> trim(optional(text()))
)(IssueStateForm.apply)
get("/:owner/:repository/issues")(referrersOnly {
searchIssues("all", _)
})
@@ -53,114 +59,112 @@ 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),
(getCollaborators(owner, name) :+ owner).sorted,
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestonesWithIssueCount(owner, name),
getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount),
repository)
} getOrElse NotFound
}
})
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
defining(repository.owner, repository.name){ case (owner, name) =>
issues.html.create(
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestones(owner, name),
getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount),
repository)
} 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)
}
})
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/%s/issues/%d".format(owner, name, 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/%s/issues/_data/%d".format(owner, name, issue.issueId))
} else Unauthorized
} getOrElse NotFound
})
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
val owner = repository.owner
val name = repository.name
val userName = context.loginAccount.get.userName
getIssue(owner, name, form.issueId.toString).map { issue =>
val action = if(isEditable(owner, name, issue.openedUserName)){
params.get("action") filter { action =>
updateClosed(owner, name, form.issueId, if(action == "close") true else false) > 0
}
} else None
val commentId = createComment(owner, name, userName, form.issueId, form.content, action)
// record activity
recordCommentIssueActivity(owner, name, userName, issue.issueId, form.content)
action match {
case Some("reopen") => recordReopenIssueActivity(owner, name, userName, issue.issueId, issue.title)
case Some("close") => recordCloseIssueActivity(owner, name, userName, issue.issueId, issue.title)
case _ =>
}
redirect("/%s/%s/issues/%d#comment-%d".format(owner, name, form.issueId, commentId))
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
}
})
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/%s/issue_comments/_data/%d".format(owner, name, comment.commentId))
} else Unauthorized
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
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 { 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) =>
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
}
})
ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository =>
defining(repository.owner, repository.name){ case (owner, name) =>
getComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){
Ok(deleteComment(comment.commentId))
} else Unauthorized
} getOrElse NotFound
}
})
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
getIssue(repository.owner, repository.name, params("id")) map { x =>
if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
@@ -172,7 +176,7 @@ trait IssuesControllerBase extends ControllerBase {
org.json4s.jackson.Serialization.write(
Map("title" -> x.title,
"content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
repository, false, true, true)
repository, false, true)
))
}
} else Unauthorized
@@ -189,7 +193,7 @@ trait IssuesControllerBase extends ControllerBase {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("content" -> view.Markdown.toHtml(x.content,
repository, false, true, true)
repository, false, true)
))
}
} else Unauthorized
@@ -197,69 +201,168 @@ 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 =>
updateAssignedUserName(repository.owner, repository.name, params("id").toInt,
params.get("assignedUserName") filter (_.trim != ""))
updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName"))
Ok("updated")
})
ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository =>
updateMilestoneId(repository.owner, repository.name, params("id").toInt,
params.get("milestoneId") collect { case x if x.trim != "" => x.toInt })
Ok("updated")
updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
milestoneId("milestoneId").map { milestoneId =>
getMilestonesWithIssueCount(repository.owner, repository.name)
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
issues.milestones.html.progress(openCount + closeCount, closeCount, false)
} getOrElse NotFound
} getOrElse Ok()
})
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
defining(params.get("value")){ action =>
executeBatch(repository) {
handleComment(_, None, repository)( _ => action)
}
}
})
post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
params("value").toIntOpt.map{ labelId =>
executeBatch(repository) { issueId =>
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
registerIssueLabel(repository.owner, repository.name, issueId, labelId)
}
}
} getOrElse NotFound
})
post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
defining(assignedUserName("value")){ value =>
executeBatch(repository) {
updateAssignedUserName(repository.owner, repository.name, _, value)
}
}
})
post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository =>
defining(milestoneId("value")){ value =>
executeBatch(repository) {
updateMilestoneId(repository.owner, repository.name, _, value)
}
}
})
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
val owner = repository.owner
val repoName = repository.name
val userName = if(filter != "all") Some(params("userName")) else None
val sessionKey = "%s/%s/issues".format(owner, repoName)
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
params("checked").split(',') map(_.toInt) foreach execute
redirect(s"/${repository.owner}/${repository.name}/issues")
}
val page = try {
val i = params.getOrElse("page", "1").toInt
if(i <= 0) 1 else i
} catch {
case e: NumberFormatException => 1
/**
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
*/
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))) = {
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) =>
updateClosed(owner, name, issueId, closed)
t
}
.getOrElse(None -> None)
val commentId = content
.map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") )
.getOrElse ( action.get.capitalize -> action.get )
match {
case (content, action) => createComment(owner, name, userName, issueId, content, action)
}
// record activity
content foreach {
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
(owner, name, userName, issueId, _)
}
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
// 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
}
}
}
// retrieve search condition
val condition = if(request.getQueryString == null){
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition]
} else IssueSearchCondition(request)
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
defining(repository.owner, repository.name){ case (owner, repoName) =>
val filterUser = Map(filter -> params.getOrElse("userName", ""))
val page = IssueSearchCondition.page(request)
val sessionKey = Keys.Session.Issues(owner, repoName)
session.put(sessionKey, condition)
// retrieve search condition
val condition = session.putAndGet(sessionKey,
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
issues.html.list(
searchIssue(owner, repoName, condition, filter, userName, (page - 1) * IssueLimit, IssueLimit),
page,
getLabels(owner, repoName),
getMilestones(owner, repoName).filter(_.closedDate.isEmpty),
countIssue(owner, repoName, condition.copy(state = "open"), filter, userName),
countIssue(owner, repoName, condition.copy(state = "closed"), filter, userName),
countIssue(owner, repoName, condition, "all", None),
context.loginAccount.map(x => countIssue(owner, repoName, condition, "assigned", Some(x.userName))),
context.loginAccount.map(x => countIssue(owner, repoName, condition, "created_by", Some(x.userName))),
countIssueGroupByLabels(owner, repoName, condition, filter, userName),
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

@@ -3,6 +3,7 @@ package app
import jp.sf.amateras.scalatra.forms._
import service._
import util.CollaboratorsAuthenticator
import org.scalatra.i18n.Messages
class LabelsController extends LabelsControllerBase
with LabelsService with RepositoryService with AccountService with CollaboratorsAuthenticator
@@ -24,7 +25,7 @@ trait LabelsControllerBase extends ControllerBase {
post("/:owner/:repository/issues/label/new", newForm)(collaboratorsOnly { (form, repository) =>
createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
redirect("/%s/%s/issues".format(repository.owner, repository.name))
redirect(s"/${repository.owner}/${repository.name}/issues")
})
ajaxGet("/:owner/:repository/issues/label/edit")(collaboratorsOnly { repository =>
@@ -51,11 +52,11 @@ 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, messages: Messages): Option[String] =
if(!value.matches("^[^,]+$")){
Some("%s contains invalid character.".format(name))
Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){
Some("%s starts with invalid character.".format(name))
Some(s"${name} starts with invalid character.")
} else {
None
}

View File

@@ -3,7 +3,8 @@ package app
import jp.sf.amateras.scalatra.forms._
import service._
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, UsersAuthenticator}
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator}
import util.Implicits._
class MilestonesController extends MilestonesControllerBase
with MilestonesService with RepositoryService with AccountService
@@ -35,38 +36,48 @@ trait MilestonesControllerBase extends ControllerBase {
post("/:owner/:repository/issues/milestones/new", milestoneForm)(collaboratorsOnly { (form, repository) =>
createMilestone(repository.owner, repository.name, form.title, form.description, form.dueDate)
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.name))
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
})
get("/:owner/:repository/issues/milestones/:milestoneId/edit")(collaboratorsOnly { repository =>
issues.milestones.html.edit(getMilestone(repository.owner, repository.name, params("milestoneId").toInt), repository)
params("milestoneId").toIntOpt.map{ milestoneId =>
issues.milestones.html.edit(getMilestone(repository.owner, repository.name, milestoneId), repository)
} getOrElse NotFound
})
post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(collaboratorsOnly { (form, repository) =>
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate))
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.name))
params("milestoneId").toIntOpt.flatMap{ milestoneId =>
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate))
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
}
} getOrElse NotFound
})
get("/:owner/:repository/issues/milestones/:milestoneId/close")(collaboratorsOnly { repository =>
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
closeMilestone(milestone)
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.name))
params("milestoneId").toIntOpt.flatMap{ milestoneId =>
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
closeMilestone(milestone)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
}
} getOrElse NotFound
})
get("/:owner/:repository/issues/milestones/:milestoneId/open")(collaboratorsOnly { repository =>
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
openMilestone(milestone)
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.name))
params("milestoneId").toIntOpt.flatMap{ milestoneId =>
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
openMilestone(milestone)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
}
} getOrElse NotFound
})
get("/:owner/:repository/issues/milestones/:milestoneId/delete")(collaboratorsOnly { repository =>
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
deleteMilestone(repository.owner, repository.name, milestone.milestoneId)
redirect("/%s/%s/issues/milestones".format(repository.owner, repository.name))
params("milestoneId").toIntOpt.flatMap{ milestoneId =>
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
deleteMilestone(repository.owner, repository.name, milestone.milestoneId)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
}
} getOrElse NotFound
})

View File

@@ -0,0 +1,474 @@
package app
import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier, Keys}
import util.Directory._
import util.Implicits._
import util.ControlUtil._
import service._
import org.eclipse.jgit.api.Git
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.transport.RefSpec
import scala.collection.JavaConverters._
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
import service.IssuesService._
import service.PullRequestService._
import util.JGitUtil.DiffInfo
import service.RepositoryService.RepositoryTreeNode
import util.JGitUtil.CommitInfo
import org.slf4j.LoggerFactory
import org.eclipse.jgit.merge.MergeStrategy
import org.eclipse.jgit.errors.NoMergeBaseException
import service.WebHookService.WebHookPayload
class PullRequestsController extends PullRequestsControllerBase
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with ActivityService with WebHookService
with ReferrerAuthenticator with CollaboratorsAuthenticator
trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with IssuesService with MilestonesService with ActivityService with PullRequestService with WebHookService
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
val pullRequestForm = mapping(
"title" -> trim(label("Title" , text(required, maxlength(100)))),
"content" -> trim(label("Content", optional(text()))),
"targetUserName" -> trim(text(required, maxlength(100))),
"targetBranch" -> trim(text(required, maxlength(100))),
"requestUserName" -> trim(text(required, maxlength(100))),
"requestRepositoryName" -> trim(text(required, maxlength(100))),
"requestBranch" -> trim(text(required, maxlength(100))),
"commitIdFrom" -> trim(text(required, maxlength(40))),
"commitIdTo" -> trim(text(required, maxlength(40)))
)(PullRequestForm.apply)
val mergeForm = mapping(
"message" -> trim(label("Message", text(required)))
)(MergeForm.apply)
case class PullRequestForm(
title: String,
content: Option[String],
targetUserName: String,
targetBranch: String,
requestUserName: String,
requestRepositoryName: String,
requestBranch: String,
commitIdFrom: String,
commitIdTo: String)
case class MergeForm(message: String)
get("/:owner/:repository/pulls")(referrersOnly { repository =>
searchPullRequests(None, repository)
})
get("/:owner/:repository/pulls/:userName")(referrersOnly { repository =>
searchPullRequests(Some(params("userName")), repository)
})
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner
val name = repository.name
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
using(Git.open(getRepositoryDir(owner, name))){ git =>
val (commits, diffs) =
getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
pulls.html.pullreq(
issue, pullreq,
getComments(owner, name, issueId),
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestonesWithIssueCount(owner, name),
commits,
diffs,
hasWritePermission(owner, name, context.loginAccount),
repository)
}
}
} getOrElse NotFound
})
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository =>
params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner
val name = repository.name
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
pulls.html.mergeguide(
checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId),
pullreq,
s"${baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
}
} getOrElse NotFound
})
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
params("id").toIntOpt.flatMap { issueId =>
val owner = repository.owner
val name = repository.name
LockUtil.lock(s"${owner}/${name}/merge"){
getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
using(Git.open(getRepositoryDir(owner, name))) { git =>
// mark issue as merged and close.
val loginAccount = context.loginAccount.get
createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
createComment(owner, name, loginAccount.userName, issueId, "Close", "close")
updateClosed(owner, name, issueId, true)
// record activity
recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
// merge
val mergeBaseRefName = s"refs/heads/${pullreq.branch}"
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
val mergeBaseTip = git.getRepository.resolve(mergeBaseRefName)
val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
val conflicted = try {
!merger.merge(mergeBaseTip, mergeTip)
} catch {
case e: NoMergeBaseException => true
}
if (conflicted) {
throw new RuntimeException("This pull request can't merge automatically.")
}
// creates merge commit
val mergeCommit = new CommitBuilder()
mergeCommit.setTreeId(merger.getResultTreeId)
mergeCommit.setParentIds(Array[ObjectId](mergeBaseTip, mergeTip): _*)
val personIdent = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
mergeCommit.setAuthor(personIdent)
mergeCommit.setCommitter(personIdent)
mergeCommit.setMessage(s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestRepositoryName}\n\n" +
form.message)
// insertObject and got mergeCommit Object Id
val inserter = git.getRepository.newObjectInserter
val mergeCommitId = inserter.insert(mergeCommit)
inserter.flush()
inserter.release()
// update refs
val refUpdate = git.getRepository.updateRef(mergeBaseRefName)
refUpdate.setNewObjectId(mergeCommitId)
refUpdate.setForceUpdate(false)
refUpdate.setRefLogIdent(personIdent)
refUpdate.setRefLogMessage("merged", true)
refUpdate.update()
val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
commits.flatten.foreach { commit =>
if(!existsCommitId(owner, name, commit.id)){
insertCommitId(owner, name, commit.id)
}
}
// call web hook
getWebHookURLs(owner, name) match {
case webHookURLs if(webHookURLs.nonEmpty) =>
for(ownerAccount <- getAccountByUserName(owner)){
callWebHook(owner, name, webHookURLs,
WebHookPayload(git, loginAccount, mergeBaseRefName, repository, commits.flatten.toList, ownerAccount))
}
case _ =>
}
// notifications
Notifier().toNotify(repository, issueId, "merge"){
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/pull/${issueId}")
}
redirect(s"/${owner}/${name}/pull/${issueId}")
}
}
}
} getOrElse NotFound
})
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(originUserName), Some(originRepositoryName)) => {
getRepository(originUserName, originRepositoryName, 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 =>
JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) =>
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
} getOrElse {
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}")
}
}
}
}
})
get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
(for(
originRepositoryName <- if(originOwner == forkedOwner){
Some(forkedRepository.name)
} else {
forkedRepository.repository.originRepositoryName.orElse {
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
}
};
originRepository <- getRepository(originOwner, originRepositoryName, baseUrl)
) yield {
using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
){ case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
val forkedId = getForkedCommitId(oldGit, newGit,
originRepository.owner, originRepository.name, originBranch,
forkedRepository.owner, forkedRepository.name, forkedBranch)
val oldId = oldGit.getRepository.resolve(forkedId)
val newId = newGit.getRepository.resolve(forkedBranch)
val (commits, diffs) = getRequestCompareInfo(
originRepository.owner, originRepository.name, oldId.getName,
forkedRepository.owner, forkedRepository.name, newId.getName)
pulls.html.compare(
commits,
diffs,
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
},
originBranch,
forkedBranch,
oldId.getName,
newId.getName,
forkedRepository,
originRepository,
forkedRepository,
hasWritePermission(forkedRepository.owner, forkedRepository.name, context.loginAccount))
}
}) getOrElse NotFound
})
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
(for(
originRepositoryName <- if(originOwner == forkedOwner){
Some(forkedRepository.name)
} else {
forkedRepository.repository.originRepositoryName.orElse {
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
}
};
originRepository <- getRepository(originOwner, originRepositoryName, baseUrl)
) yield {
using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
){ case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
pulls.html.mergecheck(
checkConflict(originRepository.owner, originRepository.name, originBranch,
forkedRepository.owner, forkedRepository.name, forkedBranch))
}
}) getOrElse NotFound
})
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
val loginUserName = context.loginAccount.get.userName
val issueId = createIssue(
owner = repository.owner,
repository = repository.name,
loginUser = loginUserName,
title = form.title,
content = form.content,
assignedUserName = None,
milestoneId = None,
isPullRequest = true)
createPullRequest(
originUserName = repository.owner,
originRepositoryName = repository.name,
issueId = issueId,
originBranch = form.targetBranch,
requestUserName = form.requestUserName,
requestRepositoryName = form.requestRepositoryName,
requestBranch = form.requestBranch,
commitIdFrom = form.commitIdFrom,
commitIdTo = form.commitIdTo)
// fetch requested branch
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.fetch
.setRemote(getRepositoryDir(form.requestUserName, form.requestRepositoryName).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head"))
.call
}
// record activity
recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title)
// notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgPullRequest(s"${baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
}
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
})
/**
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
*/
private def checkConflict(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
LockUtil.lock(s"${userName}/${repositoryName}/merge-check"){
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
val remoteRefName = s"refs/heads/${branch}"
val tmpRefName = s"refs/merge-check/${userName}/${branch}"
val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true)
try {
// fetch objects from origin repository branch
git.fetch
.setRemote(getRepositoryDir(userName, repositoryName).toURI.toString)
.setRefSpecs(refSpec)
.call
// merge conflict check
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${requestBranch}")
val mergeTip = git.getRepository.resolve(tmpRefName)
try {
!merger.merge(mergeBaseTip, mergeTip)
} catch {
case e: NoMergeBaseException => true
}
} finally {
val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
refUpdate.setForceUpdate(true)
refUpdate.delete()
}
}
}
}
/**
* Checks whether conflict will be caused in merging within pull request. Returns true if conflict will be caused.
*/
private def checkConflictInPullRequest(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String,
issueId: Int): Boolean = {
LockUtil.lock(s"${userName}/${repositoryName}/merge") {
using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
// merge
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${branch}")
val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
try {
!merger.merge(mergeBaseTip, mergeTip)
} catch {
case e: NoMergeBaseException => true
}
}
}
}
/**
* Parses branch identifier and extracts owner and branch name as tuple.
*
* - "owner:branch" to ("owner", "branch")
* - "branch" to ("defaultOwner", "branch")
*/
private def parseCompareIdentifie(value: String, defaultOwner: String): (String, String) =
if(value.contains(':')){
val array = value.split(":")
(array(0), array(1))
} else {
(defaultOwner, value)
}
/**
* 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

@@ -0,0 +1,267 @@
package app
import service._
import util.Directory._
import util.{UsersAuthenticator, OwnerAuthenticator}
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.FlashMapSupport
import org.scalatra.i18n.Messages
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 WebHookService
with OwnerAuthenticator with UsersAuthenticator
trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSupport {
self: RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator =>
// for repository options
case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean)
val optionsForm = mapping(
"repositoryName" -> trim(label("Description" , text(required, maxlength(40), identifier, renameRepositoryName))),
"description" -> trim(label("Description" , optional(text()))),
"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)
// for transfer ownership
case class TransferOwnerShipForm(newOwner: String)
val transferForm = mapping(
"newOwner" -> trim(label("New owner", text(required, transferUser)))
)(TransferOwnerShipForm.apply)
/**
* Redirect to the Options page.
*/
get("/:owner/:repository/settings")(ownerOnly { repository =>
redirect(s"/${repository.owner}/${repository.name}/settings/options")
})
/**
* Display the Options page.
*/
get("/:owner/:repository/settings/options")(ownerOnly {
settings.html.options(_, flash.get("info"))
})
/**
* Save the repository options.
*/
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
saveRepositoryOptions(
repository.owner,
repository.name,
form.description,
form.defaultBranch,
repository.repository.parentUserName.map { _ =>
repository.repository.isPrivate
} getOrElse form.isPrivate
)
// Change repository name
if(repository.name != form.repositoryName){
// Update database
renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
// Move git repository
defining(getRepositoryDir(repository.owner, repository.name)){ dir =>
FileUtils.moveDirectory(dir, getRepositoryDir(repository.owner, form.repositoryName))
}
// Move wiki repository
defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName))
}
}
flash += "info" -> "Repository settings has been updated."
redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
})
/**
* Display the Collaborators page.
*/
get("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
settings.html.collaborators(
getCollaborators(repository.owner, repository.name),
getAccountByUserName(repository.owner).get.isGroupAccount,
repository)
})
/**
* Add the collaborator.
*/
post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) =>
if(!getAccountByUserName(repository.owner).get.isGroupAccount){
addCollaborator(repository.owner, repository.name, form.userName)
}
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
})
/**
* Add the collaborator.
*/
get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository =>
if(!getAccountByUserName(repository.owner).get.isGroupAccount){
removeCollaborator(repository.owner, repository.name, params("name"))
}
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(_))
getWebHookURLs(repository.owner, repository.name) match {
case webHookURLs if(webHookURLs.nonEmpty) =>
for(ownerAccount <- getAccountByUserName(repository.owner)){
callWebHook(repository.owner, repository.name, webHookURLs,
WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount))
}
case _ =>
}
flash += "info" -> "Test payload deployed!"
}
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
})
/**
* Display the danger zone.
*/
get("/:owner/:repository/settings/danger")(ownerOnly {
settings.html.danger(_)
})
/**
* Transfer repository ownership.
*/
post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) =>
// Change repository owner
if(repository.owner != form.newOwner){
// Update database
renameRepository(repository.owner, repository.name, form.newOwner, repository.name)
// Move git repository
defining(getRepositoryDir(repository.owner, repository.name)){ dir =>
FileUtils.moveDirectory(dir, getRepositoryDir(form.newOwner, repository.name))
}
// Move wiki repository
defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name))
}
}
redirect(s"/${form.newOwner}/${repository.name}")
})
/**
* Delete the repository.
*/
post("/:owner/:repository/settings/delete")(ownerOnly { repository =>
deleteRepository(repository.owner, repository.name)
FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name))
redirect(s"/${repository.owner}")
})
/**
* Provides duplication check for web hook url.
*/
private def webHook: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.")
}
/**
* Provides Constraint to validate the collaborator name.
*/
private def collaborator: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
getAccountByUserName(value) match {
case None => Some("User does not exist.")
case Some(x) if(x.isGroupAccount)
=> Some("User does not exist.")
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
}
}
/**
* Duplicate check for the rename repository name.
*/
private def renameRepositoryName: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
params.get("repository").filter(_ != value).flatMap { _ =>
params.get("owner").flatMap { userName =>
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
}
}
}
/**
* Provides Constraint to validate the repository transfer user.
*/
private def transferUser: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
getAccountByUserName(value) match {
case None => Some("User does not exist.")
case Some(x) => if(x.userName == params("owner")){
Some("This is current repository owner.")
} else {
params.get("repository").flatMap { repositoryName =>
getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map{ _ => "User already has same repository." }
}
}
}
}
}

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
@@ -10,6 +11,7 @@ import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib._
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.treewalk._
import java.util.zip.{ZipEntry, ZipOutputStream}
class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ReferrerAuthenticator
@@ -27,8 +29,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
contentType = "text/html"
view.helpers.markdown(params("content"), repository,
params("enableWikiLink").toBoolean,
params("enableCommitLink").toBoolean,
params("enableIssueLink").toBoolean)
params("enableRefsLink").toBoolean)
})
/**
@@ -37,64 +38,46 @@ trait RepositoryViewerControllerBase extends ControllerBase {
get("/:owner/:repository")(referrersOnly {
fileList(_)
})
/**
* Displays the file list of the repository root and the specified branch.
*/
get("/:owner/:repository/tree/:id")(referrersOnly {
fileList(_, params("id"))
})
/**
* Displays the file list of the specified path and branch.
*/
get("/:owner/:repository/tree/:id/*")(referrersOnly {
fileList(_, params("id"), multiParams("splat").head)
})
/**
* Displays the commit list of the specified branch.
*/
get("/:owner/:repository/commits/:branch")(referrersOnly { repository =>
val branchName = params("branch")
val page = params.getOrElse("page", "1").toInt
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
val (logs, hasNext) = JGitUtil.getCommitLog(git, branchName, page, 30)
repo.html.commits(Nil, branchName, repository, logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
}, page, hasNext)
get("/:owner/:repository/tree/*")(referrersOnly { repository =>
val (id, path) = splitPath(repository, multiParams("splat").head)
if(path.isEmpty){
fileList(repository, id)
} else {
fileList(repository, id, path)
}
})
/**
* Displays the commit list of the specified resource.
*/
get("/:owner/:repository/commits/:branch/*")(referrersOnly { repository =>
val branchName = params("branch")
val path = multiParams("splat").head //.replaceFirst("^tree/.+?/", "")
val page = params.getOrElse("page", "1").toInt
get("/:owner/:repository/commits/*")(referrersOnly { repository =>
val (branchName, path) = splitPath(repository, multiParams("splat").head)
val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1)
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
val (logs, hasNext) = JGitUtil.getCommitLog(git, branchName, page, 30, path)
repo.html.commits(path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
}, page, hasNext)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
case Right((logs, hasNext)) =>
repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
}, page, hasNext)
case Left(_) => NotFound
}
}
})
/**
* Displays the file content of the specified branch or commit.
*/
get("/:owner/:repository/blob/:id/*")(referrersOnly { repository =>
val id = params("id") // branch name or commit id
val raw = params.get("raw").getOrElse("false").toBoolean
val path = multiParams("splat").head //.replaceFirst("^tree/.+?/", "")
get("/:owner/:repository/blob/*")(referrersOnly { repository =>
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
@@ -103,19 +86,18 @@ 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
defining(JGitUtil.getContent(git, objectId, false).get){ bytes =>
contentType = FileUtil.getContentType(path, bytes)
bytes
}
} else {
// Viewer
val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize)
@@ -125,7 +107,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)
@@ -139,35 +121,52 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
}
})
/**
* Displays details of the specified commit.
*/
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.
*/
get("/:owner/:repository/archive/:name")(referrersOnly { repository =>
val name = params("name")
if(name.endsWith(".zip")){
val revision = name.replaceFirst("\\.zip$", "")
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
@@ -175,32 +174,69 @@ trait RepositoryViewerControllerBase extends ControllerBase {
FileUtils.deleteDirectory(workDir)
}
workDir.mkdirs
// clone the repository
val cloneDir = new File(workDir, revision)
JGitUtil.withGit(Git.cloneRepository
.setURI(getRepositoryDir(repository.owner, repository.name).toURI.toString)
.setDirectory(cloneDir)
.call){ git =>
// checkout the specified revision
git.checkout.setName(revision).call
val zipFile = new File(workDir, repository.name + "-" +
(if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip")
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
using(new TreeWalk(git.getRepository)){ walk =>
val reader = walk.getObjectReader
val objectId = new MutableObjectId
using(new ZipOutputStream(new java.io.FileOutputStream(zipFile))){ out =>
walk.addTree(revCommit.getTree)
walk.setRecursive(true)
while(walk.next){
val name = walk.getPathString
val mode = walk.getFileMode(0)
if(mode != FileMode.TREE){
walk.getObjectId(objectId, 0)
val entry = new ZipEntry(name)
val loader = reader.open(objectId)
entry.setSize(loader.getSize)
out.putNextEntry(entry)
loader.copyTo(out)
}
}
}
}
}
// remove .git
FileUtils.deleteDirectory(new File(cloneDir, ".git"))
// create zip file
val zipFile = new File(workDir, (if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip")
FileUtil.createZipFile(zipFile, cloneDir)
contentType = "application/octet-stream"
response.setHeader("Content-Disposition", s"attachment; filename=${zipFile.getName}")
zipFile
} else {
BadRequest
}
})
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.split("/")(0)) get
(id, path.substring(id.length).replaceFirst("^/", ""))
}
private val readmeFiles = Seq("readme.md", "readme.markdown")
/**
* Provides HTML of the file list.
*
@@ -210,38 +246,31 @@ trait RepositoryViewerControllerBase extends ControllerBase {
* @return HTML of the file list
*/
private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = {
val revision = if(revstr.isEmpty){
repository.repository.defaultBranch
if(repository.commitCount == 0){
repo.html.guide(repository)
} else {
revstr
}
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head)
// get specified commit
JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
defining(JGitUtil.getRevCommitFromId(git, objectId)){ revCommit =>
// get files
val files = JGitUtil.getFileList(git, revision, path)
// process README.md or README.markdown
val readme = files.find { file =>
readmeFiles.contains(file.name.toLowerCase)
}.map { file =>
StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
}
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
// get latest commit
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
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")
repo.html.files(revision, repository,
if(path == ".") Nil else path.split("/").toList, // current path
new JGitUtil.CommitInfo(revCommit), // latest commit
files, readme)
}
} getOrElse NotFound
}
repo.html.files(
// current branch
revision,
// repository
repository,
// current path
if(path == ".") Nil else path.split("/").toList,
// latest commit
new JGitUtil.CommitInfo(revCommit),
// file list
files,
// readme
readme
)
}
}
}
}

View File

@@ -0,0 +1,52 @@
package app
import util._
import ControlUtil._
import service._
import jp.sf.amateras.scalatra.forms._
class SearchController extends SearchControllerBase
with RepositoryService with AccountService with SystemSettingsService with ActivityService
with RepositorySearchService with IssuesService
with ReferrerAuthenticator
trait SearchControllerBase extends ControllerBase { self: RepositoryService
with SystemSettingsService with ActivityService with RepositorySearchService
with ReferrerAuthenticator =>
val searchForm = mapping(
"query" -> trim(text(required)),
"owner" -> trim(text(required)),
"repository" -> trim(text(required))
)(SearchForm.apply)
case class SearchForm(query: String, owner: String, repository: String)
post("/search", searchForm){ form =>
redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}")
}
get("/:owner/:repository/search")(referrersOnly { repository =>
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)
case _ => search.html.code(
searchFiles(repository.owner, repository.name, query),
countIssues(repository.owner, repository.name, query),
query, page, repository)
}
}
})
}

View File

@@ -1,117 +0,0 @@
package app
import service._
import util.Directory._
import util.{UsersAuthenticator, OwnerAuthenticator}
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
class SettingsController extends SettingsControllerBase
with RepositoryService with AccountService with OwnerAuthenticator with UsersAuthenticator
trait SettingsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with OwnerAuthenticator with UsersAuthenticator =>
case class OptionsForm(description: Option[String], defaultBranch: String, isPrivate: Boolean)
val optionsForm = mapping(
"description" -> trim(label("Description" , optional(text()))),
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
"isPrivate" -> trim(label("Repository Type", boolean()))
)(OptionsForm.apply)
case class CollaboratorForm(userName: String)
val collaboratorForm = mapping(
"userName" -> trim(label("Username", text(required, collaborator)))
)(CollaboratorForm.apply)
/**
* Redirect to the Options page.
*/
get("/:owner/:repository/settings")(ownerOnly { repository =>
redirect("/%s/%s/settings/options".format(repository.owner, repository.name))
})
/**
* Display the Options page.
*/
get("/:owner/:repository/settings/options")(ownerOnly {
settings.html.options(_)
})
/**
* Save the repository options.
*/
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
saveRepositoryOptions(repository.owner, repository.name, form.description, form.defaultBranch, form.isPrivate)
redirect("/%s/%s/settings/options".format(repository.owner, repository.name))
})
/**
* Display the Collaborators page.
*/
get("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
settings.html.collaborators(getCollaborators(repository.owner, repository.name), repository)
})
/**
* JSON API for collaborator completion.
*/
get("/:owner/:repository/settings/collaborators/proposals")(usersOnly {
contentType = formats("json")
org.json4s.jackson.Serialization.write(Map("options" -> getAllUsers.map(_.userName).toArray))
})
/**
* Add the collaborator.
*/
post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) =>
addCollaborator(repository.owner, repository.name, form.userName)
redirect("/%s/%s/settings/collaborators".format(repository.owner, repository.name))
})
/**
* Add the collaborator.
*/
get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository =>
removeCollaborator(repository.owner, repository.name, params("name"))
redirect("/%s/%s/settings/collaborators".format(repository.owner, repository.name))
})
/**
* Display the delete repository page.
*/
get("/:owner/:repository/settings/delete")(ownerOnly {
settings.html.delete(_)
})
/**
* Delete the repository.
*/
post("/:owner/:repository/settings/delete")(ownerOnly { repository =>
deleteRepository(repository.owner, repository.name)
FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name))
redirect("/%s".format(repository.owner))
})
/**
* 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("/")
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))
=> Some("User can access this repository already.")
case _ => None
}
}
}
}

View File

@@ -1,48 +0,0 @@
package app
import service._
import util.StringUtil._
import jp.sf.amateras.scalatra.forms._
class SignInController extends SignInControllerBase with SystemSettingsService with AccountService
trait SignInControllerBase extends ControllerBase { self: SystemSettingsService with AccountService =>
case class SignInForm(userName: String, password: String)
val form = mapping(
"userName" -> trim(label("Username", text(required))),
"password" -> trim(label("Password", text(required)))
)(SignInForm.apply)
get("/signin"){
val queryString = request.getQueryString
if(queryString != null && queryString.startsWith("/")){
session.setAttribute("REDIRECT", queryString)
}
html.signin(loadSystemSettings())
}
post("/signin", form){ form =>
val account = getAccountByUserName(form.userName)
if(account.isEmpty || account.get.password != encrypt(form.password)){
redirect("/signin")
} else {
session.setAttribute("LOGIN_ACCOUNT", account.get)
updateLastLoginDate(account.get.userName)
session.get("REDIRECT").map { redirectUrl =>
session.removeAttribute("REDIRECT")
redirect(redirectUrl.asInstanceOf[String])
}.getOrElse {
redirect("/%s".format(account.get.userName))
}
}
}
get("/signout"){
session.invalidate
redirect("/")
}
}

View File

@@ -4,26 +4,50 @@ import service.{AccountService, SystemSettingsService}
import SystemSettingsService._
import util.AdminAuthenticator
import jp.sf.amateras.scalatra.forms._
import org.scalatra.FlashMapSupport
class SystemSettingsController extends SystemSettingsControllerBase
with SystemSettingsService with AccountService with AdminAuthenticator
trait SystemSettingsControllerBase extends ControllerBase {
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()))),
"fromAddress" -> trim(label("FROM Address", optional(text()))),
"fromName" -> trim(label("FROM Name", optional(text())))
)(Smtp.apply)),
"ldapAuthentication" -> trim(label("LDAP", boolean())),
"ldap" -> optionalIfNotChecked("ldapAuthentication", mapping(
"host" -> trim(label("LDAP host", text(required))),
"port" -> trim(label("LDAP port", optional(number()))),
"bindDN" -> trim(label("Bind DN", optional(text()))),
"bindPassword" -> trim(label("Bind Password", optional(text()))),
"baseDN" -> trim(label("Base DN", text(required))),
"userNameAttribute" -> trim(label("User name attribute", text(required))),
"fullNameAttribute" -> trim(label("Full name attribute", optional(text()))),
"mailAttribute" -> trim(label("Mail address attribute", text(required))),
"tls" -> trim(label("Enable TLS", optional(boolean()))),
"keystore" -> trim(label("Keystore", optional(text())))
)(Ldap.apply))
)(SystemSettings.apply)
get("/admin/system")(adminOnly {
admin.html.system(loadSystemSettings())
admin.html.system(loadSystemSettings(), flash.get("info"))
})
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,74 +3,177 @@ package app
import service._
import util.AdminAuthenticator
import util.StringUtil._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import util.Directory._
class UserManagementController extends UserManagementControllerBase with AccountService with AdminAuthenticator
class UserManagementController extends UserManagementControllerBase
with AccountService with RepositoryService with AdminAuthenticator
trait UserManagementControllerBase extends ControllerBase { self: AccountService with AdminAuthenticator =>
trait UserManagementControllerBase extends AccountManagementControllerBase {
self: AccountService with RepositoryService with AdminAuthenticator =>
case class UserNewForm(userName: String, password: String, mailAddress: String, isAdmin: Boolean, url: Option[String])
case class UserEditForm(userName: String, password: Option[String], mailAddress: String, isAdmin: Boolean, url: Option[String])
case class NewUserForm(userName: String, password: String, fullName: String,
mailAddress: String, isAdmin: Boolean,
url: Option[String], fileId: Option[String])
val newForm = mapping(
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" , text(required, maxlength(20)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
"isAdmin" -> trim(label("User Type" , boolean())),
"url" -> trim(label("URL" , optional(text(maxlength(200)))))
)(UserNewForm.apply)
case class EditUserForm(userName: String, password: Option[String], fullName: String,
mailAddress: String, isAdmin: Boolean, url: Option[String],
fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
memberNames: Option[String])
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
memberNames: Option[String], clearImage: Boolean, isRemoved: Boolean)
val newUserForm = mapping(
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" ,text(required, maxlength(20)))),
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))),
"isAdmin" -> trim(label("User Type" ,boolean())),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text())))
)(NewUserForm.apply)
val editUserForm = mapping(
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))),
"password" -> trim(label("Password" ,optional(text(maxlength(20))))),
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))),
"isAdmin" -> trim(label("User Type" ,boolean())),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"clearImage" -> trim(label("Clear image" ,boolean())),
"removed" -> trim(label("Disable" ,boolean()))
)(EditUserForm.apply)
val newGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"memberNames" -> trim(label("Member Names" ,optional(text())))
)(NewGroupForm.apply)
val editGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"memberNames" -> trim(label("Member Names" ,optional(text()))),
"clearImage" -> trim(label("Clear image" ,boolean())),
"removed" -> trim(label("Disable" ,boolean()))
)(EditGroupForm.apply)
val editForm = mapping(
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier))),
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
"isAdmin" -> trim(label("User Type" , boolean())),
"url" -> trim(label("URL" , optional(text(maxlength(200)))))
)(UserEditForm.apply)
get("/admin/users")(adminOnly {
admin.users.html.list(getAllUsers())
val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
val users = getAllUsers(includeRemoved)
val members = users.collect { case account if(account.isGroupAccount) =>
account.userName -> getGroupMembers(account.userName)
}.toMap
admin.users.html.list(users, members, includeRemoved)
})
get("/admin/users/_new")(adminOnly {
admin.users.html.edit(None)
get("/admin/users/_newuser")(adminOnly {
admin.users.html.user(None)
})
post("/admin/users/_new", newForm)(adminOnly { form =>
createAccount(form.userName, encrypt(form.password), form.mailAddress, form.isAdmin, form.url)
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url)
updateImage(form.userName, form.fileId, false)
redirect("/admin/users")
})
get("/admin/users/:userName/_edit")(adminOnly {
get("/admin/users/:userName/_edituser")(adminOnly {
val userName = params("userName")
admin.users.html.edit(getAccountByUserName(userName))
admin.users.html.user(getAccountByUserName(userName, true))
})
post("/admin/users/:name/_edit", editForm)(adminOnly { form =>
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
val userName = params("userName")
getAccountByUserName(userName).map { account =>
updateAccount(getAccountByUserName(userName).get.copy(
password = form.password.map(encrypt).getOrElse(account.password),
getAccountByUserName(userName, true).map { account =>
if(form.isRemoved){
// Remove repositories
getRepositoryNamesOfUser(userName).foreach { repositoryName =>
deleteRepository(userName, repositoryName)
FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
}
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
removeUserRelatedData(userName)
}
updateAccount(account.copy(
password = form.password.map(sha1).getOrElse(account.password),
fullName = form.fullName,
mailAddress = form.mailAddress,
isAdmin = form.isAdmin,
url = form.url))
url = form.url,
isRemoved = form.isRemoved))
updateImage(userName, form.fileId, form.clearImage)
redirect("/admin/users")
} getOrElse NotFound
})
// TODO Merge with AccountController?
private def uniqueUserName: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
getAccountByUserName(value).map { _ => "User already exists." }
}
get("/admin/users/_newgroup")(adminOnly {
admin.users.html.group(None, Nil)
})
// TODO Merge with AccountController?
private def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] =
getAccountByMailAddress(value)
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
.map { _ => "Mail address is already registered." }
}
}
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
createGroup(form.groupName, form.url)
updateGroupMembers(form.groupName, form.memberNames.map(_.split(",").toList).getOrElse(Nil))
updateImage(form.groupName, form.fileId, false)
redirect("/admin/users")
})
get("/admin/users/:groupName/_editgroup")(adminOnly {
defining(params("groupName")){ groupName =>
admin.users.html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
}
})
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
defining(params("groupName"), form.memberNames.map(_.split(",").toList).getOrElse(Nil)){ case (groupName, memberNames) =>
getAccountByUserName(groupName, true).map { account =>
updateGroup(groupName, form.url, form.isRemoved)
if(form.isRemoved){
// Remove from GROUP_MEMBER
updateGroupMembers(form.groupName, Nil)
// Remove repositories
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
deleteRepository(groupName, repositoryName)
FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
}
} else {
// Update GROUP_MEMBER
updateGroupMembers(form.groupName, memberNames)
// Update COLLABORATOR for group repositories
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")
} getOrElse NotFound
}
})
post("/admin/users/_usercheck")(adminOnly {
getAccountByUserName(params("userName")).isDefined
})
}

View File

@@ -1,88 +1,123 @@
package app
import service._
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, JGitUtil}
import util._
import util.Directory._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.api.Git
import org.scalatra.FlashMapSupport
import org.scalatra.i18n.Messages
import scala.Some
import java.util.ResourceBundle
class WikiController extends WikiControllerBase
with WikiService with RepositoryService with AccountService with ActivityService
with CollaboratorsAuthenticator with ReferrerAuthenticator
trait WikiControllerBase extends ControllerBase {
trait WikiControllerBase extends ControllerBase with FlashMapSupport {
self: WikiService with RepositoryService with ActivityService
with CollaboratorsAuthenticator with ReferrerAuthenticator =>
case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String)
case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String)
val newForm = mapping(
"pageName" -> trim(label("Page name" , text(required, maxlength(40), identifier, unique))),
"content" -> trim(label("Content" , text(required))),
"message" -> trim(label("Message" , optional(text()))),
"currentPageName" -> trim(label("Current page name" , text()))
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename, unique))),
"content" -> trim(label("Content" , text(required, conflictForNew))),
"message" -> trim(label("Message" , optional(text()))),
"currentPageName" -> trim(label("Current page name" , text())),
"id" -> trim(label("Latest commit id" , text()))
)(WikiPageEditForm.apply)
val editForm = mapping(
"pageName" -> trim(label("Page name" , text(required, maxlength(40), identifier))),
"content" -> trim(label("Content" , text(required))),
"message" -> trim(label("Message" , optional(text()))),
"currentPageName" -> trim(label("Current page name" , text(required)))
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename))),
"content" -> trim(label("Content" , text(required, conflictForEdit))),
"message" -> trim(label("Message" , optional(text()))),
"currentPageName" -> trim(label("Current page name" , text(required))),
"id" -> trim(label("Latest commit id" , text(required)))
)(WikiPageEditForm.apply)
get("/:owner/:repository/wiki")(referrersOnly { repository =>
getWikiPage(repository.owner, repository.name, "Home").map { page =>
wiki.html.page("Home", page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
} getOrElse redirect("/%s/%s/wiki/Home/_edit".format(repository.owner, repository.name))
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit")
})
get("/:owner/:repository/wiki/:page")(referrersOnly { repository =>
val pageName = params("page")
val pageName = StringUtil.urlDecode(params("page"))
getWikiPage(repository.owner, repository.name, pageName).map { page =>
wiki.html.page(pageName, page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
} getOrElse redirect("/%s/%s/wiki/%s/_edit".format(repository.owner, repository.name, pageName)) // TODO URLEncode
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
})
get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository =>
val pageName = params("page")
val pageName = StringUtil.urlDecode(params("page"))
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
wiki.html.history(Some(pageName), JGitUtil.getCommitLog(git, "master", path = pageName + ".md")._1, repository)
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
}
}
})
get("/:owner/:repository/wiki/:page/_compare/:commitId")(referrersOnly { repository =>
val pageName = params("page")
val commitId = params("commitId").split("\\.\\.\\.")
val pageName = StringUtil.urlDecode(params("page"))
val Array(from, to) = 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), from, to, JGitUtil.getDiffs(git, from, to, true).filter(_.newPath == pageName + ".md"), repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
}
})
get("/:owner/:repository/wiki/_compare/:commitId")(referrersOnly { repository =>
val commitId = params("commitId").split("\\.\\.\\.")
val Array(from, to) = 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, from, to, JGitUtil.getDiffs(git, from, to, true), repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
}
})
get("/:owner/:repository/wiki/:page/_revert/:commitId")(collaboratorsOnly { repository =>
val pageName = StringUtil.urlDecode(params("page"))
val Array(from, to) = params("commitId").split("\\.\\.\\.")
if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, Some(pageName))){
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}")
} else {
flash += "info" -> "This patch was not able to be reversed."
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_compare/${from}...${to}")
}
})
get("/:owner/:repository/wiki/_revert/:commitId")(collaboratorsOnly { repository =>
val Array(from, to) = params("commitId").split("\\.\\.\\.")
if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, None)){
redirect(s"/${repository.owner}/${repository.name}/wiki/")
} else {
flash += "info" -> "This patch was not able to be reversed."
redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}")
}
})
get("/:owner/:repository/wiki/:page/_edit")(collaboratorsOnly { repository =>
val pageName = params("page")
val pageName = StringUtil.urlDecode(params("page"))
wiki.html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
})
post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) =>
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)
redirect("/%s/%s/wiki/%s".format(repository.owner, repository.name, form.pageName))
defining(context.loginAccount.get){ loginAccount =>
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
form.content, loginAccount, form.message.getOrElse(""), Some(form.id)).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)}")
}
})
get("/:owner/:repository/wiki/_new")(collaboratorsOnly {
@@ -90,24 +125,26 @@ trait WikiControllerBase extends ControllerBase {
})
post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) =>
val loginAccount = context.loginAccount.get
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
form.content, context.loginAccount.get, form.message.getOrElse(""))
updateLastActivityDate(repository.owner, repository.name)
recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
defining(context.loginAccount.get){ loginAccount =>
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
form.content, loginAccount, form.message.getOrElse(""), None)
redirect("/%s/%s/wiki/%s".format(repository.owner, repository.name, form.pageName))
updateLastActivityDate(repository.owner, repository.name)
recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
}
})
get("/:owner/:repository/wiki/:page/_delete")(collaboratorsOnly { repository =>
val pageName = params("page")
deleteWikiPage(repository.owner, repository.name, pageName, context.loginAccount.get.userName, "Delete %s".format(pageName))
updateLastActivityDate(repository.owner, repository.name)
val pageName = StringUtil.urlDecode(params("page"))
redirect("/%s/%s/wiki".format(repository.owner, repository.name))
defining(context.loginAccount.get){ loginAccount =>
deleteWikiPage(repository.owner, repository.name, pageName, loginAccount.fullName, loginAccount.mailAddress, s"Destroyed ${pageName}")
updateLastActivityDate(repository.owner, repository.name)
redirect(s"/${repository.owner}/${repository.name}/wiki")
}
})
get("/:owner/:repository/wiki/_pages")(referrersOnly { repository =>
@@ -116,21 +153,55 @@ trait WikiControllerBase extends ControllerBase {
})
get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
wiki.html.history(None, JGitUtil.getCommitLog(git, "master")._1, repository)
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
}
}
})
get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository =>
getFileContent(repository.owner, repository.name, multiParams("splat").head).map { content =>
contentType = "application/octet-stream"
content
val path = multiParams("splat").head
getFileContent(repository.owner, repository.name, path).map { bytes =>
contentType = FileUtil.getContentType(path, bytes)
bytes
} 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], messages: Messages): Option[String] =
getWikiPageList(params("owner"), params("repository")).find(_ == value).map(_ => "Page already exists.")
}
}
private def pagename: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
if(value.exists("\\/:*?\"<>|".contains(_))){
Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){
Some(s"${name} starts with invalid character.")
} else {
None
}
}
private def conflictForNew: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = {
targetWikiPage.map { _ =>
"Someone has created the wiki since you started. Please reload this page and re-apply your changes."
}
}
}
private def conflictForEdit: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = {
targetWikiPage.filter(_.id != params("id")).map{ _ =>
"Someone has edited the wiki since you started. Please reload this page and re-apply your changes."
}
}
}
private def targetWikiPage = getWikiPage(params("owner"), params("repository"), params("pageName"))
}

View File

@@ -4,6 +4,7 @@ import scala.slick.driver.H2Driver.simple._
object Accounts extends Table[Account]("ACCOUNT") {
def userName = column[String]("USER_NAME", O PrimaryKey)
def fullName = column[String]("FULL_NAME")
def mailAddress = column[String]("MAIL_ADDRESS")
def password = column[String]("PASSWORD")
def isAdmin = column[Boolean]("ADMINISTRATOR")
@@ -11,16 +12,23 @@ object Accounts extends Table[Account]("ACCOUNT") {
def registeredDate = column[java.util.Date]("REGISTERED_DATE")
def updatedDate = column[java.util.Date]("UPDATED_DATE")
def lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE")
def * = userName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? <> (Account, Account.unapply _)
def image = column[String]("IMAGE")
def groupAccount = column[Boolean]("GROUP_ACCOUNT")
def removed = column[Boolean]("REMOVED")
def * = userName ~ fullName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? ~ groupAccount ~ removed <> (Account, Account.unapply _)
}
case class Account(
userName: String,
fullName: String,
mailAddress: String,
password: String,
isAdmin: Boolean,
url: Option[String],
registeredDate: java.util.Date,
updatedDate: java.util.Date,
lastLoginDate: Option[java.util.Date]
lastLoginDate: Option[java.util.Date],
image: Option[String],
isGroupAccount: Boolean,
isRemoved: Boolean
)

View File

@@ -0,0 +1,14 @@
package model
import scala.slick.driver.H2Driver.simple._
object GroupMembers extends Table[GroupMember]("GROUP_MEMBER") {
def groupName = column[String]("GROUP_NAME", O PrimaryKey)
def userName = column[String]("USER_NAME", O PrimaryKey)
def * = groupName ~ userName <> (GroupMember, GroupMember.unapply _)
}
case class GroupMember(
groupName: String,
userName: String
)

View File

@@ -7,6 +7,11 @@ object IssueId extends Table[(String, String, Int)]("ISSUE_ID") with IssueTempla
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
}
object IssueOutline extends Table[(String, String, Int, Int)]("ISSUE_OUTLINE_VIEW") with IssueTemplate {
def commentCount = column[Int]("COMMENT_COUNT")
def * = userName ~ repositoryName ~ issueId ~ commentCount
}
object Issues extends Table[Issue]("ISSUE") with IssueTemplate with MilestoneTemplate {
def openedUserName = column[String]("OPENED_USER_NAME")
def assignedUserName = column[String]("ASSIGNED_USER_NAME")
@@ -15,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)
}
@@ -31,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

@@ -9,9 +9,9 @@ object IssueComments extends Table[IssueComment]("ISSUE_COMMENT") with IssueTemp
def content = column[String]("CONTENT")
def registeredDate = column[java.util.Date]("REGISTERED_DATE")
def updatedDate = column[java.util.Date]("UPDATED_DATE")
def * = userName ~ repositoryName ~ issueId ~ commentId ~ action.? ~ commentedUserName ~ content ~ registeredDate ~ updatedDate <> (IssueComment, IssueComment.unapply _)
def * = userName ~ repositoryName ~ issueId ~ commentId ~ action ~ commentedUserName ~ content ~ registeredDate ~ updatedDate <> (IssueComment, IssueComment.unapply _)
def autoInc = userName ~ repositoryName ~ issueId ~ action.? ~ commentedUserName ~ content ~ registeredDate ~ updatedDate returning commentId
def autoInc = userName ~ repositoryName ~ issueId ~ action ~ commentedUserName ~ content ~ registeredDate ~ updatedDate returning commentId
def byPrimaryKey(commentId: Int) = this.commentId is commentId.bind
}
@@ -20,7 +20,7 @@ case class IssueComment(
repositoryName: String,
issueId: Int,
commentId: Int,
action: Option[String],
action: String,
commentedUserName: String,
content: String,
registeredDate: java.util.Date,

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

@@ -1,5 +1,6 @@
package object model {
import scala.slick.lifted.MappedTypeMapper
import scala.slick.driver.BasicDriver.Implicit._
import scala.slick.lifted.{Column, MappedTypeMapper}
// java.util.Date TypeMapper
implicit val dateTypeMapper = MappedTypeMapper.base[java.util.Date, java.sql.Timestamp](
@@ -7,6 +8,10 @@ package object model {
t => new java.util.Date(t.getTime)
)
implicit class RichColumn(c1: Column[Boolean]){
def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
}
/**
* Returns system date.
*/

View File

@@ -3,42 +3,152 @@ 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
import org.slf4j.LoggerFactory
trait AccountService {
def getAccountByUserName(userName: String): Option[Account] =
Query(Accounts) filter(_.userName is userName.bind) firstOption
private val logger = LoggerFactory.getLogger(classOf[AccountService])
def getAccountByMailAddress(mailAddress: String): Option[Account] =
Query(Accounts) filter(_.mailAddress is mailAddress.bind) firstOption
def authenticate(settings: SystemSettings, userName: String, password: String): Option[Account] =
if(settings.ldapAuthentication){
ldapAuthentication(settings, userName, password)
} else {
defaultAuthentication(userName, password)
}
def getAllUsers(): List[Account] = Query(Accounts) sortBy(_.userName) list
/**
* 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(ldapUserInfo) => {
// Create or update account by LDAP information
getAccountByUserName(userName, true) match {
case Some(x) if(!x.isRemoved) => updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName))
case Some(x) if(x.isRemoved) => {
logger.info(s"LDAP Authentication Failed: Account is already registered but disabled..")
defaultAuthentication(userName, password)
}
case None => createAccount(userName, "", ldapUserInfo.fullName, ldapUserInfo.mailAddress, false, None)
}
getAccountByUserName(userName)
}
case Left(errorMessage) => {
logger.info(s"LDAP Authentication Failed: ${errorMessage}")
defaultAuthentication(userName, password)
}
}
}
def getAccountByUserName(userName: String, includeRemoved: Boolean = false): Option[Account] =
Query(Accounts) filter(t => (t.userName is userName.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false): Option[Account] =
Query(Accounts) filter(t => (t.mailAddress is mailAddress.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
def getAllUsers(includeRemoved: Boolean = true): List[Account] =
if(includeRemoved){
Query(Accounts) sortBy(_.userName) list
} else {
Query(Accounts) filter (_.removed is false.bind) sortBy(_.userName) list
}
def createAccount(userName: String, password: String, mailAddress: String, isAdmin: Boolean, url: Option[String]): Unit =
def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, url: Option[String]): Unit =
Accounts insert Account(
userName = userName,
password = password,
fullName = fullName,
mailAddress = mailAddress,
isAdmin = isAdmin,
url = url,
registeredDate = currentDate,
updatedDate = currentDate,
lastLoginDate = None)
lastLoginDate = None,
image = None,
isGroupAccount = false,
isRemoved = false)
def updateAccount(account: Account): Unit =
Accounts
.filter { a => a.userName is account.userName.bind }
.map { a => a.password ~ a.mailAddress ~ a.isAdmin ~ a.url.? ~ a.registeredDate ~ a.updatedDate ~ a.lastLoginDate.? }
.map { a => a.password ~ a.fullName ~ a.mailAddress ~ a.isAdmin ~ a.url.? ~ a.registeredDate ~ a.updatedDate ~ a.lastLoginDate.? ~ a.removed }
.update (
account.password,
account.fullName,
account.mailAddress,
account.isAdmin,
account.url,
account.registeredDate,
currentDate,
account.lastLoginDate)
account.lastLoginDate,
account.isRemoved)
def updateAvatarImage(userName: String, image: Option[String]): Unit =
Accounts.filter(_.userName is userName.bind).map(_.image.?).update(image)
def updateLastLoginDate(userName: String): Unit =
Accounts.filter(_.userName is userName.bind).map(_.lastLoginDate).update(currentDate)
def createGroup(groupName: String, url: Option[String]): Unit =
Accounts insert Account(
userName = groupName,
password = "",
fullName = groupName,
mailAddress = groupName + "@devnull",
isAdmin = false,
url = url,
registeredDate = currentDate,
updatedDate = currentDate,
lastLoginDate = None,
image = None,
isGroupAccount = true,
isRemoved = false)
def updateGroup(groupName: String, url: Option[String], removed: Boolean): Unit =
Accounts.filter(_.userName is groupName.bind).map(t => t.url.? ~ t.removed).update(url, removed)
def updateGroupMembers(groupName: String, members: List[String]): Unit = {
Query(GroupMembers).filter(_.groupName is groupName.bind).delete
members.foreach { userName =>
GroupMembers insert GroupMember (groupName, userName)
}
}
def getGroupMembers(groupName: String): List[String] =
Query(GroupMembers)
.filter(_.groupName is groupName.bind)
.sortBy(_.userName)
.map(_.userName)
.list
def getGroupsByUserName(userName: String): List[String] =
Query(GroupMembers)
.filter(_.userName is userName.bind)
.sortBy(_.groupName)
.map(_.groupName)
.list
def removeUserRelatedData(userName: String): Unit = {
Query(GroupMembers).filter(_.userName is userName.bind).delete
Query(Collaborators).filter(_.collaboratorName is userName.bind).delete
Query(Repositories).filter(_.userName is userName.bind).delete
}
}
object AccountService extends AccountService

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,10 +52,17 @@ 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",
s"[user:${activityUserName}] closed reopened [issue:${userName}/${repositoryName}#${issueId}]",
s"[user:${activityUserName}] reopened issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(title),
currentDate)
@@ -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,
@@ -95,18 +109,60 @@ trait ActivityService {
s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]",
None,
currentDate)
def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) =
def recordDeleteTagActivity(userName: String, repositoryName: String, activityUserName: String,
tagName: String, commits: List[util.JGitUtil.CommitInfo]) =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"create_tag",
s"[user:${activityUserName}] created branch [tag:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
"delete_tag",
s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]",
None,
currentDate)
def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"create_branch",
s"[user:${activityUserName}] created branch [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
None,
currentDate)
def recordDeleteBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"delete_branch",
s"[user:${activityUserName}] deleted branch ${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)
}
def insertAllCommitIds(userName: String, repositoryName: String, commitIds: List[String]) =
CommitLog insertAll (commitIds.map(commitId => (userName, repositoryName, commitId)): _*)
def getAllCommitIds(userName: String, repositoryName: String): List[String] =
Query(CommitLog).filter(_.byRepository(userName, repositoryName)).map(_.commitId).list
def existsCommitId(userName: String, repositoryName: String, commitId: String): Boolean =
Query(CommitLog).filter(_.byPrimaryKey(userName, repositoryName, commitId)).firstOption.isDefined

View File

@@ -7,6 +7,7 @@ import Q.interpolation
import model._
import util.Implicits._
import util.StringUtil._
trait IssuesService {
import IssuesService._
@@ -35,36 +36,34 @@ trait IssuesService {
.map ( _._2 )
.list
def getIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) =
Query(IssueLabels) filter (_.byPrimaryKey(owner, repository, issueId, labelId)) firstOption
/**
* Returns the count of the search result against issues.
*
* @param owner the repository owner
* @param repository the repository name
* @param condition the search condition
* @param filter the filter type ("all", "assigned" or "created_by")
* @param userName the filter user name required for "assigned" and "created_by"
* @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(owner: String, repository: String, condition: IssueSearchCondition, filter: String, userName: Option[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(owner, repository, condition, filter, userName) map (_.issueId) list).length
}
def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
repos: (String, String)*): Int =
Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first
/**
* Returns the Map which contains issue count for each labels.
*
* @param owner the repository owner
* @param repository the repository name
* @param condition the search condition
* @param filter the filter type ("all", "assigned" or "created_by")
* @param userName the filter user name required for "assigned" and "created_by"
* @return the Map which contains issue count for each labels (key is label name, value is issue count),
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @return the Map which contains issue count for each labels (key is label name, value is issue count)
*/
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
filter: String, userName: Option[String]): Map[String, Int] = {
filterUser: Map[String, String]): Map[String, Int] = {
searchIssueQuery(owner, repository, condition.copy(labels = Set.empty), filter, userName)
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false)
.innerJoin(IssueLabels).on { (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
}
@@ -79,73 +78,100 @@ trait IssuesService {
}
.toMap
}
/**
* Returns list which contains issue count for each repository.
* If the issue does not exist, its repository is not included in the result.
*
* @param condition the search condition
* @param 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], 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
}
.sortBy(_._3 desc)
.list
}
/**
* Returns the search result against issues.
*
* @param owner the repository owner
* @param repository the repository name
* @param condition the search condition
* @param filter the filter type ("all", "assigned" or "created_by")
* @param userName the filter user name required for "assigned" and "created_by"
* @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(owner: String, repository: String, condition: IssueSearchCondition,
filter: String, userName: Option[String], offset: Int, limit: Int): List[(Issue, List[Label], Int)] = {
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
val issues = searchIssueQuery(owner, repository, condition, filter, userName)
.leftJoin(Query(IssueComments)
.filter { _.byRepository(owner, repository) }
.groupBy { _.issueId }
.map { case (issueId, t) => issueId ~ t.length }).on((t1, t2) => t1.issueId is t2._1)
.sortBy { case (t1, t2) =>
(condition.sort match {
case "created" => t1.registeredDate
case "comments" => t2._2
case "updated" => t1.updatedDate
}) match {
case sort => condition.direction match {
case "asc" => sort asc
case "desc" => sort desc
// get issues and comment count and labels
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) }
.map { case (((t1, t2), t3), t4) =>
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?)
}
.sortBy(_._4) // labelName
.sortBy { case (t1, commentCount, _,_,_) =>
(condition.sort match {
case "created" => t1.registeredDate
case "comments" => commentCount
case "updated" => t1.updatedDate
}) match {
case sort => condition.direction match {
case "asc" => sort asc
case "desc" => sort desc
}
}
}
}
.map { case (t1, t2) => (t1, t2._2.ifNull(0)) }
.drop(offset).take(limit)
.list
// get labels
val labels = Query(IssueLabels)
.innerJoin(Labels).on { (t1, t2) =>
t1.byLabel(t2.userName, t2.repositoryName, t2.labelId)
}
.filter { case (t1, t2) =>
(t1.byRepository(owner, repository)) &&
(t1.issueId inSetBind (issues.map(_._1.issueId)))
}
.sortBy { case (t1, t2) => t1.issueId ~ t2.labelName }
.map { case (t1, t2) => (t1.issueId, t2) }
.list
issues.map { case (issue, commentCount) =>
(issue, labels.collect { case (issueId, labels) if(issueId == issue.issueId) => labels }, commentCount)
}
.drop(offset).take(limit)
.list
.splitWith { (c1, c2) =>
c1._1.userName == c2._1.userName &&
c1._1.repositoryName == c2._1.repositoryName &&
c1._1.issueId == c2._1.issueId
}
.map { issues => issues.head match {
case (issue, commentCount, _,_,_) =>
(issue,
issues.flatMap { t => t._3.map (
Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
)} toList,
commentCount)
}} toList
}
/**
* Assembles query for conditional issue searching.
*/
private def searchIssueQuery(owner: String, repository: String, condition: IssueSearchCondition, filter: String, userName: Option[String]) =
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition,
filterUser: Map[String, String], onlyPullRequest: Boolean) =
Query(Issues) filter { t1 =>
(t1.byRepository(owner, repository)) &&
condition.repo
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
.getOrElse (repos)
.map { case (owner, repository) => t1.byRepository(owner, repository) }
.foldLeft[Column[Boolean]](false) ( _ || _ ) &&
(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 userName.get.bind, filter == "assigned") &&
(t1.openedUserName is userName.get.bind, filter == "created_by") &&
(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
@@ -157,7 +183,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 =>
@@ -172,7 +198,8 @@ trait IssuesService {
content,
false,
currentDate,
currentDate)
currentDate,
isPullRequest)
// increment issue id
IssueId
@@ -188,7 +215,7 @@ trait IssuesService {
IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete
def createComment(owner: String, repository: String, loginUser: String,
issueId: Int, content: String, action: Option[String]) =
issueId: Int, content: String, action: String) =
IssueComments.autoInc insert (
owner,
repository,
@@ -222,6 +249,9 @@ trait IssuesService {
}
.update (content, currentDate)
def deleteComment(commentId: Int) =
IssueComments filter (_.byPrimaryKey(commentId)) delete
def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean) =
Issues
.filter (_.byPrimaryKey(owner, repository, issueId))
@@ -230,10 +260,63 @@ trait IssuesService {
}
.update (closed, currentDate)
/**
* Search issues by keyword.
*
* @param owner the repository owner
* @param repository the repository name
* @param query the keywords separated by whitespace.
* @return issues with comment count and matched content of issue or comment
*/
def searchIssuesByKeyword(owner: String, repository: String, query: String): List[(Issue, Int, String)] = {
import scala.slick.driver.H2Driver.likeEncode
val keywords = splitWords(query.toLowerCase)
// Search Issue
val issues = Issues
.innerJoin(IssueOutline).on { case (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
}
.filter { case (t1, t2) =>
keywords.map { keyword =>
(t1.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) ||
(t1.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^'))
} .reduceLeft(_ && _)
}
.map { case (t1, t2) =>
(t1, 0, t1.content.?, t2.commentCount)
}
// Search IssueComment
val comments = IssueComments
.innerJoin(Issues).on { case (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
}
.innerJoin(IssueOutline).on { case ((t1, t2), t3) =>
t2.byIssue(t3.userName, t3.repositoryName, t3.issueId)
}
.filter { case ((t1, t2), t3) =>
keywords.map { query =>
t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^')
}.reduceLeft(_ && _)
}
.map { case ((t1, t2), t3) =>
(t2, t1.commentId, t1.content.?, t3.commentCount)
}
issues.union(comments).sortBy { case (issue, commentId, _, _) =>
issue.issueId ~ commentId
}.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) =>
issue1.issueId == issue2.issueId
}.map { _.head match {
case (issue, _, content, commentCount) => (issue, commentCount, content.getOrElse(""))
}
}.toList
}
}
object IssuesService {
import java.net.URLEncoder
import javax.servlet.http.HttpServletRequest
val IssueLimit = 30
@@ -241,12 +324,11 @@ object IssuesService {
case class IssueSearchCondition(
labels: Set[String] = Set.empty,
milestoneId: Option[Option[Int]] = None,
repo: Option[String] = None,
state: String = "open",
sort: String = "created",
direction: String = "desc"){
import IssueSearchCondition._
def toURL: String =
"?" + List(
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(" "))),
@@ -254,6 +336,7 @@ object IssuesService {
case Some(x) => x.toString
case None => "none"
})},
repo.map("for=" + urlEncode(_)),
Some("state=" + urlEncode(state)),
Some("sort=" + urlEncode(sort)),
Some("direction=" + urlEncode(direction))).flatten.mkString("&")
@@ -262,8 +345,6 @@ object IssuesService {
object IssueSearchCondition {
private def urlEncode(value: String): String = URLEncoder.encode(value, "UTF-8")
private def param(request: HttpServletRequest, name: String, allow: Seq[String] = Nil): Option[String] = {
val value = request.getParameter(name)
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value)
@@ -272,12 +353,21 @@ object IssuesService {
def apply(request: HttpServletRequest): IssueSearchCondition =
IssueSearchCondition(
param(request, "labels").map(_.split(" ").toSet).getOrElse(Set.empty),
param(request, "milestone").map(_ match {
param(request, "milestone").map{
case "none" => None
case x => Some(x.toInt)
}),
case x => x.toIntOpt
},
param(request, "for"),
param(request, "state", Seq("open", "closed")).getOrElse("open"),
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"))
def page(request: HttpServletRequest) = try {
val i = param(request, "page").getOrElse("1").toInt
if(i <= 0) 1 else i
} catch {
case e: NumberFormatException => 1
}
}
}

View File

@@ -0,0 +1,69 @@
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)] =
getIssue(owner, repository, issueId.toString).flatMap{ issue =>
Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).firstOption.map{
pullreq => (issue, pullreq)
}
}
def updateCommitIdTo(owner: String, repository: String, issueId: Int, commitIdTo: String): Unit =
Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).map(_.commitIdTo).update(commitIdTo)
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))
def getPullRequestsByRequest(userName: String, repositoryName: String, branch: String, closed: Boolean): List[PullRequest] =
Query(PullRequests)
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
.filter { case (t1, t2) =>
(t1.requestUserName is userName.bind) &&
(t1.requestRepositoryName is repositoryName.bind) &&
(t1.requestBranch is branch.bind) &&
(t2.closed is closed.bind)
}
.map { case (t1, t2) => t1 }
.list
}
object PullRequestService {
val PullRequestLimit = 25
case class PullRequestCount(userName: String, count: Int)
}

View File

@@ -0,0 +1,126 @@
package service
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
import scala.collection.mutable.ListBuffer
import org.eclipse.jgit.lib.FileMode
import org.eclipse.jgit.api.Git
trait
RepositorySearchService { self: IssuesService =>
import RepositorySearchService._
def countIssues(owner: String, repository: String, query: String): Int =
searchIssuesByKeyword(owner, repository, query).length
def searchIssues(owner: String, repository: String, query: String): List[IssueSearchResult] =
searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) =>
IssueSearchResult(
issue.issueId,
issue.title,
issue.openedUserName,
issue.registeredDate,
commentCount,
getHighlightText(content, query)._1)
}
def countFiles(owner: String, repository: String, query: String): Int =
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] =
using(Git.open(getRepositoryDir(owner, repository))){ git =>
if(JGitUtil.isEmpty(git)){
Nil
} else {
val files = searchRepositoryFiles(git, query)
val commits = JGitUtil.getLatestCommitFromPaths(git, files.toList.map(_._1), "HEAD")
files.map { case (path, text) =>
val (highlightText, lineNumber) = getHighlightText(text, query)
FileSearchResult(
path,
commits(path).getCommitterIdent.getWhen,
highlightText,
lineNumber)
}
}
}
private def searchRepositoryFiles(git: Git, query: String): List[(String, String)] = {
val revWalk = new RevWalk(git.getRepository)
val objectId = git.getRepository.resolve("HEAD")
val revCommit = revWalk.parseCommit(objectId)
val treeWalk = new TreeWalk(git.getRepository)
treeWalk.setRecursive(true)
treeWalk.addTree(revCommit.getTree)
val keywords = StringUtil.splitWords(query.toLowerCase)
val list = new ListBuffer[(String, String)]
while (treeWalk.next()) {
if(treeWalk.getFileMode(0) != FileMode.TREE){
JGitUtil.getContent(git, treeWalk.getObjectId(0), false).foreach { bytes =>
if(FileUtil.isText(bytes)){
val text = StringUtil.convertFromByteArray(bytes)
val lowerText = text.toLowerCase
val indices = keywords.map(lowerText.indexOf _)
if(!indices.exists(_ < 0)){
list.append((treeWalk.getPathString, text))
}
}
}
}
}
treeWalk.release
revWalk.release
list.toList
}
}
object RepositorySearchService {
val CodeLimit = 10
val IssueLimit = 10
def getHighlightText(content: String, query: String): (String, Int) = {
val keywords = StringUtil.splitWords(query.toLowerCase)
val lowerText = content.toLowerCase
val indices = keywords.map(lowerText.indexOf _)
if(!indices.exists(_ < 0)){
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 class=\"highlight\">$1</span>")
(highlightText, lineNumber + 1)
} else {
(content.split("\n").take(5).mkString("\n"), 1)
}
}
case class SearchResult(
files : List[(String, String)],
issues: List[(Issue, Int, String)])
case class IssueSearchResult(
issueId: Int,
title: String,
openedUserName: String,
registeredDate: java.util.Date,
commentCount: Int,
highlightText: String)
case class FileSearchResult(
path: String,
lastModified: java.util.Date,
highlightText: String,
highlightLineNumber: Int)
}

View File

@@ -11,36 +11,107 @@ trait RepositoryService { self: AccountService =>
/**
* Creates a new repository.
*
* The project is created as public repository at first. Users can modify the project type at the repository settings
* page after the project creation to configure the project as the private repository.
*
* @param repositoryName the repository name
* @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]): 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 = false,
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)
}
def renameRepository(oldUserName: String, oldRepositoryName: String, newUserName: String, newRepositoryName: String): Unit = {
(Query(Repositories) filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository =>
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
val webHooks = Query(WebHooks ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val milestones = Query(Milestones ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueId = Query(IssueId ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issues = Query(Issues ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val pullRequests = Query(PullRequests ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val labels = Query(Labels ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueComments = Query(IssueComments).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueLabels = Query(IssueLabels ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val collaborators = Query(Collaborators).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val commitLog = Query(CommitLog ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val activities = Query(Activities ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
Repositories.filter { t =>
(t.originUserName is oldUserName.bind) && (t.originRepositoryName is oldRepositoryName.bind)
}.map { t => t.originUserName ~ t.originRepositoryName }.update(newUserName, newRepositoryName)
Repositories.filter { t =>
(t.parentUserName is oldUserName.bind) && (t.parentRepositoryName is oldRepositoryName.bind)
}.map { t => t.originUserName ~ t.originRepositoryName }.update(newUserName, newRepositoryName)
PullRequests.filter { t =>
t.requestRepositoryName is oldRepositoryName.bind
}.map { t => t.requestUserName ~ t.requestRepositoryName }.update(newUserName, newRepositoryName)
deleteRepository(oldUserName, oldRepositoryName)
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
Issues .insertAll(issues .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueLabels .insertAll(issueLabels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Collaborators .insertAll(collaborators .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
CommitLog .insertAll(commitLog .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
Activities .insertAll(activities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
// Update activity messages
val updateActivities = Activities.filter { t =>
(t.message like s"%:${oldUserName}/${oldRepositoryName}]%") ||
(t.message like s"%:${oldUserName}/${oldRepositoryName}#%")
}.map { t => t.activityId ~ t.message }.list
updateActivities.foreach { case (activityId, message) =>
Activities.filter(_.activityId is activityId.bind).map(_.message).update(
message
.replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]")
.replace(s"[branch:${oldUserName}/${oldRepositoryName}#" ,s"[branch:${newUserName}/${newRepositoryName}#")
.replace(s"[tag:${oldUserName}/${oldRepositoryName}#" ,s"[tag:${newUserName}/${newRepositoryName}#")
.replace(s"[pullreq:${oldUserName}/${oldRepositoryName}#",s"[pullreq:${newUserName}/${newRepositoryName}#")
.replace(s"[issue:${oldUserName}/${oldRepositoryName}#" ,s"[issue:${newUserName}/${newRepositoryName}#")
)
}
}
}
def deleteRepository(userName: String, repositoryName: String): Unit = {
Activities .filter(_.byRepository(userName, repositoryName)).delete
CommitLog .filter(_.byRepository(userName, repositoryName)).delete
Collaborators .filter(_.byRepository(userName, repositoryName)).delete
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
Labels .filter(_.byRepository(userName, repositoryName)).delete
IssueComments .filter(_.byRepository(userName, repositoryName)).delete
PullRequests .filter(_.byRepository(userName, repositoryName)).delete
Issues .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
}
@@ -53,39 +124,6 @@ trait RepositoryService { self: AccountService =>
def getRepositoryNamesOfUser(userName: String): List[String] =
Query(Repositories) filter(_.userName is userName.bind) map (_.repositoryName) list
/**
* Returns the list of specified user's repositories information.
*
* @param userName the user name
* @param baseUrl the base url of this application
* @param loginUserName the logged in user name
* @return the list of repository information which is sorted in descending order of lastActivityDate.
*/
def getVisibleRepositories(userName: String, baseUrl: String, loginUserName: Option[String]): List[RepositoryInfo] = {
val q1 = Repositories
.filter { t => t.userName is userName.bind }
.map { r => r }
val q2 = Collaborators
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
.filter{ case (t1, t2) => t1.collaboratorName is userName.bind}
.map { case (t1, t2) => t2 }
def visibleFor(t: Repositories.type, loginUserName: Option[String]) = {
loginUserName match {
case Some(x) => (t.isPrivate is false.bind) || (
(t.isPrivate is true.bind) && ((t.userName is x.bind) || (Collaborators.filter { c =>
c.byRepository(t.userName, t.repositoryName) && (c.collaboratorName is x.bind)
}.exists)))
case None => (t.isPrivate is false.bind)
}
}
q1.union(q2).filter(visibleFor(_, loginUserName)).sortBy(_.lastActivityDate desc).list map { repository =>
new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository)
}
}
/**
* Returns the specified repository information.
*
@@ -96,34 +134,69 @@ 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)
))
}
}
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,
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
))
}
}
/**
* Returns the list of accessible repositories information for the specified account user.
*
* @param account the account
* 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
* @return the repository informations which is sorted in descending order of lastActivityDate.
* @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
* @return the repository information which is sorted in descending order of lastActivityDate.
*/
def getAccessibleRepositories(account: Option[Account], baseUrl: String): List[RepositoryInfo] = {
def newRepositoryInfo(repository: Repository): RepositoryInfo = {
new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository)
}
(account match {
def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None): List[RepositoryInfo] = {
(loginAccount match {
// for Administrators
case Some(x) if(x.isAdmin) => Query(Repositories)
// for Normal Users
case Some(x) if(!x.isAdmin) =>
Query(Repositories) filter { t => (t.isPrivate is false.bind) ||
(Query(Collaborators).filter(t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)) exists)
Query(Repositories) filter { t => (t.isPrivate is false.bind) || (t.userName is x.userName) ||
(Query(Collaborators).filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)} exists)
}
// for Guests
case None => Query(Repositories) filter(_.isPrivate is false.bind)
}).sortBy(_.lastActivityDate desc).list.map(newRepositoryInfo _)
}).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,
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
))
}
}
/**
@@ -161,6 +234,15 @@ trait RepositoryService { self: AccountService =>
def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String): Unit =
Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete
/**
* Remove all collaborators from the repository.
*
* @param userName the user name of the repository owner
* @param repositoryName the repository name
*/
def removeCollaborators(userName: String, repositoryName: String): Unit =
Collaborators.filter(_.byRepository(userName, repositoryName)).delete
/**
* Returns the list of collaborators name which is sorted with ascending order.
*
@@ -180,17 +262,39 @@ trait RepositoryService { self: AccountService =>
}
}
private def getForkedCount(userName: String, repositoryName: String): Int =
Query(Repositories.filter { t =>
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
}.length).first
def getForkedRepositories(userName: String, repositoryName: String): List[(String, String)] =
Query(Repositories).filter { t =>
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
}
.sortBy(_.userName asc).map(t => t.userName ~ t.repositoryName).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

@@ -0,0 +1,37 @@
package service
import model._
import service.SystemSettingsService.SystemSettings
/**
* This service is used for a view helper mainly.
*
* It may be called many times in one request, so each method stores
* its result into the cache which available during a request.
*/
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)
}
}
def getAccountByUserName(userName: String)(implicit context: app.Context): Option[Account] = {
context.cache(s"account.${userName}"){
new AccountService {}.getAccountByUserName(userName)
}
}
def getAccountByMailAddress(mailAddress: String)(implicit context: app.Context): Option[Account] = {
context.cache(s"account.${mailAddress}"){
new AccountService {}.getAccountByMailAddress(mailAddress)
}
}
}

View File

@@ -1,40 +1,164 @@
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))
smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _))
smtp.fromName.foreach(props.setProperty(SmtpFromName, _))
}
}
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)
ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute)
ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
}
}
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),
getOptionValue(props, SmtpFromAddress, None),
getOptionValue(props, SmtpFromName, 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, ""),
getOptionValue(props, LdapFullNameAttribute, None),
getValue(props, LdapMailAddressAttribute, ""),
getOptionValue[Boolean](props, LdapTls, None),
getOptionValue(props, LdapKeystore, None)))
} 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,
fullNameAttribute: Option[String],
mailAttribute: String,
tls: Option[Boolean],
keystore: Option[String])
case class Smtp(
host: String,
port: Option[Int],
user: Option[String],
password: Option[String],
ssl: Option[Boolean],
fromAddress: Option[String],
fromName: Option[String])
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 SmtpFromAddress = "smtp.from_address"
private val SmtpFromName = "smtp.from_name"
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 LdapFullNameAttribute = "ldap.fullname_attribute"
private val LdapMailAddressAttribute = "ldap.mail_attribute"
private val LdapTls = "ldap.tls"
private val LdapKeystore = "ldap.keystore"
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, webHookURLs: List[WebHook], 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.HttpClientBuilder
import scala.concurrent._
import ExecutionContext.Implicits.global
logger.debug("start callWebHook")
implicit val formats = Serialization.formats(NoTypeHints)
if(webHookURLs.nonEmpty){
val json = write(payload)
val httpClient = HttpClientBuilder.create.build
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(
pusher: WebHookUser,
ref: String,
commits: List[WebHookCommit],
repository: WebHookRepository)
object WebHookPayload {
def apply(git: Git, pusher: Account, refName: String, repositoryInfo: RepositoryInfo,
commits: List[CommitInfo], repositoryOwner: Account): WebHookPayload =
WebHookPayload(
WebHookUser(pusher.fullName, pusher.mailAddress),
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

@@ -1,14 +1,21 @@
package service
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.lib.RepositoryBuilder
import org.eclipse.jgit.treewalk.CanonicalTreeParser
import java.util.concurrent.ConcurrentHashMap
import util._
import _root_.util.ControlUtil._
import org.eclipse.jgit.treewalk.{TreeWalk, CanonicalTreeParser}
import org.eclipse.jgit.lib._
import org.eclipse.jgit.dircache.{DirCache, DirCacheEntry}
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter}
import java.io.ByteArrayInputStream
import org.eclipse.jgit.patch._
import org.eclipse.jgit.api.errors.PatchFormatException
import scala.collection.JavaConverters._
import scala.Some
object WikiService {
@@ -19,8 +26,9 @@ object WikiService {
* @param content the page content
* @param committer the last committer
* @param time the last modified time
* @param id the latest commit id
*/
case class WikiPageInfo(name: String, content: String, committer: String, time: Date)
case class WikiPageInfo(name: String, content: String, committer: String, time: Date, id: String)
/**
* The model for wiki page history.
@@ -32,77 +40,41 @@ 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(owner: model.Account, repository: String): Unit = {
lock(owner.userName, repository){
val dir = Directory.getWikiRepositoryDir(owner.userName, repository)
if(!dir.exists){
val repo = new RepositoryBuilder().setGitDir(dir).setBare.build
try {
repo.create
saveWikiPage(owner.userName, repository, "Home", "Home", "Welcome to the %s wiki!!".format(repository), owner, "Initial Commit")
} finally {
repo.close
// once delete cloned repository because initial cloned repository does not have 'branch.master.merge'
FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner.userName, 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){
JGitUtil.initRepository(dir)
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None)
}
}
}
}
/**
* Returns the wiki page.
*/
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git =>
try {
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
if(!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)
WikiPageInfo(file.name, StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes),
file.committer, file.time, file.commitId)
}
} catch {
// TODO no commit, but it should not judge by exception.
case e: NullPointerException => None
}
} 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 =>
try {
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] =
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
if(!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)
@@ -110,58 +82,183 @@ trait WikiService {
JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file =>
git.getRepository.open(file.id).getBytes
}
} catch {
// TODO no commit, but it should not judge by exception.
case e: NullPointerException => None
}
} else None
}
}
/**
* Returns the list of wiki page names.
*/
def getWikiPageList(owner: String, repository: String): List[String] = {
JGitUtil.getFileList(Git.open(Directory.getWikiRepositoryDir(owner, repository)), "master", ".")
.filter(_.name.endsWith(".md"))
.map(_.name.replaceFirst("\\.md$", ""))
.sortBy(x => x)
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
JGitUtil.getFileList(git, "master", ".")
.filter(_.name.endsWith(".md"))
.map(_.name.replaceFirst("\\.md$", ""))
.sortBy(x => x)
}
}
/**
* Reverts specified changes.
*/
def revertWikiPage(owner: String, repository: String, from: String, to: String,
committer: model.Account, pageName: Option[String]): Boolean = {
case class RevertInfo(operation: String, filePath: String, source: String)
try {
LockUtil.lock(s"${owner}/${repository}/wiki"){
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
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}"))
val diffs = git.diff.setNewTree(oldTreeIter).setOldTree(newTreeIter).call.asScala.filter { diff =>
pageName match {
case Some(x) => diff.getNewPath == x + ".md"
case None => true
}
}
val patch = using(new java.io.ByteArrayOutputStream()){ out =>
val formatter = new DiffFormatter(out)
formatter.setRepository(git.getRepository)
formatter.format(diffs.asJava)
new String(out.toByteArray, "UTF-8")
}
val p = new Patch()
p.parse(new ByteArrayInputStream(patch.getBytes("UTF-8")))
if(!p.getErrors.isEmpty){
throw new PatchFormatException(p.getErrors())
}
val revertInfo = (p.getFiles.asScala.map { fh =>
fh.getChangeType match {
case DiffEntry.ChangeType.MODIFY => {
val source = getWikiPage(owner, repository, fh.getNewPath.replaceFirst("\\.md$", "")).map(_.content).getOrElse("")
val applied = PatchUtil.apply(source, patch, fh)
if(applied != null){
Seq(RevertInfo("ADD", fh.getNewPath, applied))
} else Nil
}
case DiffEntry.ChangeType.ADD => {
val applied = PatchUtil.apply("", patch, fh)
if(applied != null){
Seq(RevertInfo("ADD", fh.getNewPath, applied))
} else Nil
}
case DiffEntry.ChangeType.DELETE => {
Seq(RevertInfo("DELETE", fh.getNewPath, ""))
}
case DiffEntry.ChangeType.RENAME => {
val applied = PatchUtil.apply("", patch, fh)
if(applied != null){
Seq(RevertInfo("DELETE", fh.getOldPath, ""), RevertInfo("ADD", fh.getNewPath, applied))
} else {
Seq(RevertInfo("DELETE", fh.getOldPath, ""))
}
}
case _ => Nil
}
}).flatten
if(revertInfo.nonEmpty){
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
using(new RevWalk(git.getRepository)){ revWalk =>
using(new TreeWalk(git.getRepository)){ treeWalk =>
val index = treeWalk.addTree(revWalk.parseTree(headId))
treeWalk.setRecursive(true)
while(treeWalk.next){
val path = treeWalk.getPathString
val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
if(revertInfo.find(x => x.filePath == path).isEmpty){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
}
}
}
}
revertInfo.filter(_.operation == "ADD").foreach { x =>
builder.add(JGitUtil.createDirCacheEntry(x.filePath, FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, x.source.getBytes("UTF-8"))))
}
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
pageName match {
case Some(x) => s"Revert ${from} ... ${to} on ${x}"
case None => s"Revert ${from} ... ${to}"
})
}
}
}
true
} catch {
case e: Exception => {
e.printStackTrace()
false
}
}
}
/**
* 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, currentId: Option[String]): Option[String] = {
LockUtil.lock(s"${owner}/${repository}/wiki"){
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
var created = true
var updated = false
var removed = false
lock(owner, repository){
// clone working copy
val workDir = Directory.getWikiWorkDir(owner, repository)
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
if(headId != null){
using(new RevWalk(git.getRepository)){ revWalk =>
using(new TreeWalk(git.getRepository)){ treeWalk =>
val index = treeWalk.addTree(revWalk.parseTree(headId))
treeWalk.setRecursive(true)
while(treeWalk.next){
val path = treeWalk.getPathString
val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
if(path == currentPageName + ".md" && currentPageName != newPageName){
removed = true
} else if(path != newPageName + ".md"){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
} else {
created = false
updated = JGitUtil.getContent(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false)
}
}
}
}
}
// delete file
val deleted = if(currentPageName != "" && currentPageName != newPageName){
git.rm.addFilepattern(currentPageName + ".md").call
true
} else {
false
}
if(created || updated || removed){
builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
if(message.trim.length == 0) {
if(removed){
s"Rename ${currentPageName} to ${newPageName}"
} else if(created){
s"Created ${newPageName}"
} else {
s"Updated ${newPageName}"
}
} else {
message
})
// commit and push
if(added || deleted){
git.commit.setCommitter(committer.userName, committer.mailAddress).setMessage(message).call
git.push.call
}
Some(newHeadId)
} else None
}
}
}
@@ -169,56 +266,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"){
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
var removed = false
// 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
using(new RevWalk(git.getRepository)){ revWalk =>
using(new TreeWalk(git.getRepository)){ treeWalk =>
val index = treeWalk.addTree(revWalk.parseTree(headId))
treeWalk.setRecursive(true)
while(treeWalk.next){
val path = treeWalk.getPathString
val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
if(path != pageName + ".md"){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
} else {
removed = true
}
}
}
if(removed){
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message)
}
}
}
}
}
}
/**
* 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){
Git.cloneRepository
.setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString)
.setDirectory(workDir)
.call
} else {
Git.open(workDir).pull.call
}
}
}
}

View File

@@ -1,12 +1,14 @@
package servlet
import java.io.File
import java.sql.Connection
import java.sql.{DriverManager, Connection}
import org.apache.commons.io.FileUtils
import javax.servlet.ServletContextEvent
import javax.servlet.{ServletContext, ServletContextListener, 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 {
@@ -25,16 +27,15 @@ object AutoUpdate {
* If corresponding SQL file does not exist, this method do nothing.
*/
def update(conn: Connection): Unit = {
val sqlPath = "update/%d_%d.sql".format(majorVersion, minorVersion)
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()
val sqlPath = s"update/${majorVersion}_${minorVersion}.sql"
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)
}
}
}
}
@@ -42,16 +43,42 @@ object AutoUpdate {
/**
* MAJOR.MINOR
*/
val versionString = "%d.%d".format(majorVersion, minorVersion)
val versionString = s"${majorVersion}.${minorVersion}"
}
/**
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
Version(1, 10),
Version(1, 9),
Version(1, 8),
Version(1, 7),
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
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
}
}
}
}
}
}
},
Version(1, 2),
Version(1, 1),
Version(1, 0)
Version(1, 0),
Version(0, 0)
)
/**
@@ -62,7 +89,7 @@ object AutoUpdate {
/**
* The version file (GITBUCKET_HOME/version).
*/
val versionFile = new File(Directory.GitBucketHome, "version")
lazy val versionFile = new File(GitBucketHome, "version")
/**
* Returns the current version from the version file.
@@ -77,44 +104,60 @@ object AutoUpdate {
}
case _ => Version(0, 0)
}
} else {
Version(0, 0)
}
}
} else Version(0, 0)
}
}
/**
* Start H2 database and update schema automatically.
* Update database schema automatically in the context initializing.
*/
class AutoUpdateListener extends org.h2.server.web.DbStarter {
class AutoUpdateListener extends ServletContextListener {
import AutoUpdate._
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
override def contextInitialized(event: ServletContextEvent): Unit = {
super.contextInitialized(event)
logger.debug("H2 started")
val datadir = event.getServletContext.getInitParameter("gitbucket.home")
if(datadir != null){
System.setProperty("gitbucket.home", datadir)
}
org.h2.Driver.load()
event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome}")
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)
conn.rollback()
defining(getConnection(event.getServletContext)){ conn =>
try {
defining(getCurrentVersion()){ currentVersion =>
if(currentVersion == headVersion){
logger.debug("No update")
} else if(!versions.contains(currentVersion)){
logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.")
} else {
versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
conn.commit()
logger.debug(s"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")
}
}
def contextDestroyed(sce: ServletContextEvent): Unit = {
// Nothing to do.
}
private def getConnection(servletContext: ServletContext): Connection =
DriverManager.getConnection(
servletContext.getInitParameter("db.url"),
servletContext.getInitParameter("db.user"),
servletContext.getInitParameter("db.password"))
}

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,28 +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") && !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)
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(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 => {
@@ -57,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 == encrypt(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,13 @@ 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
import util.JGitUtil.CommitInfo
/**
* Provides Git repository via HTTP.
@@ -24,7 +29,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 +38,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,68 +56,142 @@ 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 pusher = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
logger.debug("requestURI: " + request.getRequestURI)
logger.debug("userName:" + userName)
logger.debug("pusher:" + pusher)
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)
if(!repository.endsWith(".wiki")){
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, 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, pusher: String, baseURL: String) extends PostReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService 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 =>
commands.asScala.foreach { command =>
val commits = JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
val refName = command.getRefName.split("/")
// apply issue comment
val newCommits = commits.flatMap { commit =>
if(!existsCommitId(owner, repository, commit.id)){
insertCommitId(owner, repository, commit.id)
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData =>
val issueId = matchData.group(2)
if(getAccountByUserName(commit.committer).isDefined && getIssue(owner, repository, issueId).isDefined){
createComment(owner, repository, commit.committer, issueId.toInt, commit.fullMessage, Some("commit"))
}
}
Some(commit)
} else None
}.toList
// record activity
if(refName(1) == "heads"){
command.getType match {
case ReceiveCommand.Type.CREATE => {
recordCreateBranchActivity(owner, repository, userName, refName(2))
recordPushActivity(owner, repository, userName, refName(2), newCommits)
}
case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, userName, refName(2), newCommits)
case _ =>
try {
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
commands.asScala.foreach { command =>
logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
val commits = command.getType match {
case ReceiveCommand.Type.DELETE => Nil
case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
}
} else if(refName(1) == "tags"){
command.getType match {
case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, userName, refName(2), newCommits)
val refName = command.getRefName.split("/")
val branchName = refName.drop(2).mkString("/")
// Extract new commit and apply issue comment
val newCommits = if(commits.size > 1000){
val existIds = getAllCommitIds(owner, repository)
commits.flatMap { commit =>
if(!existIds.contains(commit.id)){
createIssueComment(commit)
Some(commit)
} else None
}
} else {
commits.flatMap { commit =>
if(!existsCommitId(owner, repository, commit.id)){
createIssueComment(commit)
Some(commit)
} else None
}
}
// batch insert all new commit id
insertAllCommitIds(owner, repository, newCommits.map(_.id))
// record activity
if(refName(1) == "heads"){
command.getType match {
case ReceiveCommand.Type.CREATE => recordCreateBranchActivity(owner, repository, pusher, branchName)
case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, pusher, branchName, newCommits)
case ReceiveCommand.Type.DELETE => recordDeleteBranchActivity(owner, repository, pusher, branchName)
case _ =>
}
} else if(refName(1) == "tags"){
command.getType match {
case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, pusher, branchName, newCommits)
case ReceiveCommand.Type.DELETE => recordDeleteTagActivity(owner, repository, pusher, branchName, newCommits)
case _ =>
}
}
if(refName(1) == "heads"){
command.getType match {
case ReceiveCommand.Type.CREATE |
ReceiveCommand.Type.UPDATE |
ReceiveCommand.Type.UPDATE_NONFASTFORWARD =>
updatePullRequests(branchName)
case _ =>
}
}
// call web hook
getWebHookURLs(owner, repository) match {
case webHookURLs if(webHookURLs.nonEmpty) =>
for(pusherAccount <- getAccountByUserName(pusher);
ownerAccount <- getAccountByUserName(owner);
repositoryInfo <- getRepository(owner, repository, baseURL)){
callWebHook(owner, repository, webHookURLs,
WebHookPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount))
}
case _ =>
}
}
}
// update repository last modified time.
updateLastActivityDate(owner, repository)
} catch {
case ex: Exception => {
logger.error(ex.toString, ex)
throw ex
}
}
// update repository last modified time.
updateLastActivityDate(owner, repository)
}
private def createIssueComment(commit: CommitInfo) = {
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData =>
val issueId = matchData.group(2)
if(getIssue(owner, repository, issueId).isDefined){
getAccountByMailAddress(commit.mailAddress).foreach { account =>
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
}
}
}
}
/**
* Fetch pull request contents into refs/pull/${issueId}/head and update pull request table.
*/
private def updatePullRequests(branch: String) =
getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq =>
if(getRepository(pullreq.userName, pullreq.repositoryName, baseURL).isDefined){
using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName))){ git =>
git.fetch
.setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/pull/${pullreq.issueId}/head").setForceUpdate(true))
.call
val commitIdTo = git.getRepository.resolve(s"refs/pull/${pullreq.issueId}/head").getName
updateCommitIdTo(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo)
}
}
}
}

View File

@@ -0,0 +1,15 @@
package servlet
import javax.servlet.http.{HttpSessionEvent, HttpSessionListener}
import app.FileUploadControllerBase
/**
* Removes session associated temporary files when session is destroyed.
*/
class SessionCleanupListener extends HttpSessionListener with FileUploadControllerBase {
def sessionCreated(se: HttpSessionEvent): Unit = {}
def sessionDestroyed(se: HttpSessionEvent): Unit = removeTemporaryFiles()(se.getSession)
}

View File

@@ -3,10 +3,9 @@ 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.
* Controls the transaction with the open session in view pattern.
*/
class TransactionFilter extends Filter {
@@ -21,16 +20,19 @@ class TransactionFilter extends Filter {
// assets don't need transaction
chain.doFilter(req, res)
} else {
// TODO begin transaction!
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,53 @@
package util
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.TreeWalk
import org.eclipse.jgit.transport.RefSpec
import scala.language.reflectiveCalls
/**
* 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 withTmpRefSpec[T](ref: RefSpec, git: Git)(f: RefSpec => T): T = {
// try {
// f(ref)
// } finally {
// val refUpdate = git.getRepository.updateRef(ref.getDestination)
// refUpdate.setForceUpdate(true)
// refUpdate.delete()
// }
// }
}

View File

@@ -1,71 +1,80 @@
package util
import java.io.File
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Ref
import util.ControlUtil._
import org.apache.commons.io.FileUtils
/**
* Provides directories used by GitBucket.
*/
object Directory {
val GitBucketHome = new File(System.getProperty("user.home"), "gitbucket").getAbsolutePath
val GitBucketHome = (System.getProperty("gitbucket.home") match {
// -Dgitbucket.home=<path>
case path if(path != null) => new File(path)
case _ => scala.util.Properties.envOrNone("GITBUCKET_HOME") match {
// environment variable GITBUCKET_HOME
case Some(env) => new File(env)
// default is HOME/.gitbucket
case None => {
val oldHome = new File(System.getProperty("user.home"), "gitbucket")
if(oldHome.exists && oldHome.isDirectory && new File(oldHome, "version").exists){
//FileUtils.moveDirectory(oldHome, newHome)
oldHome
} else {
new File(System.getProperty("user.home"), ".gitbucket")
}
}
}
}).getAbsolutePath
val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
val RepositoryHome = "%s/repositories".format(GitBucketHome)
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/%s".format(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.
*/
def getRepositoryDir(owner: String, repository: String): File =
new File("%s/%s/%s.git".format(RepositoryHome, owner, repository))
new File(s"${RepositoryHome}/${owner}/${repository}.git")
/**
* Directory for uploaded files by the specified user.
*/
def getUserUploadDir(userName: String): File = new File(s"${GitBucketHome}/data/${userName}/files")
/**
* Root of temporary directories for the specified repository.
*/
def getTemporaryDir(owner: String, repository: String): File =
new File("%s/tmp/%s/%s".format(GitBucketHome, owner, repository))
new File(s"${GitBucketHome}/tmp/${owner}/${repository}")
/**
* Temporary directory which is used to create an archive to download repository contents.
*/
def getDownloadWorkDir(owner: String, repository: String, sessionId: String): File =
new File(getTemporaryDir(owner, repository), "download/%s".format(sessionId))
/**
* Temporary directory which is used in the repository creation.
*
* GitBucket generates initial repository contents in this directory and push them.
* This directory is removed after the repository creation.
*/
def getInitRepositoryDir(owner: String, repository: String): File =
new File(getTemporaryDir(owner, repository), "init")
new File(getTemporaryDir(owner, repository), s"download/${sessionId}")
/**
* Substance directory of the wiki repository.
*/
def getWikiRepositoryDir(owner: String, repository: String): File =
new File("%s/%s/%s.wiki.git".format(Directory.RepositoryHome, owner, repository))
/**
* Wiki working directory which is cloned from the wiki repository.
*/
def getWikiWorkDir(owner: String, repository: String): File =
new File(getTemporaryDir(owner, repository), "wiki")
new File(s"${RepositoryHome}/${owner}/${repository}.wiki.git")
}

View File

@@ -1,47 +1,72 @@
package util
import org.apache.commons.io.{IOUtils, FileUtils, FilenameUtils}
import org.apache.commons.io.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 getContentType(name: String, bytes: Array[Byte]): String = {
defining(getMimeType(name)){ mimeType =>
if(mimeType == "application/octet-stream" && isText(bytes)){
"text/plain"
} else {
mimeType
}
}
}
def isImage(name: String): Boolean = getMimeType(name).startsWith("image/")
def isLarge(size: Long): Boolean = (size > 1024 * 1000)
def isText(content: Array[Byte]): Boolean = !content.contains(0)
def createZipFile(dest: File, dir: File): Unit = {
def addDirectoryToZip(out: ZipArchiveOutputStream, dir: File, path: String): Unit = {
dir.listFiles.map { file =>
if(file.isFile){
out.putArchiveEntry(new ZipArchiveEntry(path + "/" + file.getName))
out.write(FileUtils.readFileToByteArray(file))
out.closeArchiveEntry
} else if(file.isDirectory){
addDirectoryToZip(out, file, path + "/" + file.getName)
}
}
}
// def createZipFile(dest: File, dir: File): Unit = {
// def addDirectoryToZip(out: ZipArchiveOutputStream, dir: File, path: String): Unit = {
// dir.listFiles.map { file =>
// if(file.isFile){
// out.putArchiveEntry(new ZipArchiveEntry(path + "/" + file.getName))
// out.write(FileUtils.readFileToByteArray(file))
// out.closeArchiveEntry
// } else if(file.isDirectory){
// addDirectoryToZip(out, file, path + "/" + file.getName)
// }
// }
// }
//
// using(new ZipArchiveOutputStream(dest)){ out =>
// addDirectoryToZip(out, dir, dir.getName)
// }
// }
val out = new ZipArchiveOutputStream(dest)
try {
addDirectoryToZip(out, dir, dir.getName)
} finally {
IOUtils.closeQuietly(out)
}
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 _ => ""
}
def withTmpDir[A](dir: File)(action: File => A): A = {
if(dir.exists()){
FileUtils.deleteDirectory(dir)
}
try {
action(dir)
} finally {
FileUtils.deleteDirectory(dir)
}
}
}

View File

@@ -1,19 +1,19 @@
package util
import twirl.api.Html
import scala.slick.driver.H2Driver.simple._
import scala.util.matching.Regex
import javax.servlet.http.{HttpSession, HttpServletRequest}
/**
* Provides some usable implicit conversions.
*/
object Implicits {
implicit def extendsSeq[A](seq: Seq[A]) = new {
implicit class RichSeq[A](seq: Seq[A]) {
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 {
@@ -22,12 +22,57 @@ object Implicits {
}
case Nil => result
}
}
implicit class RichString(value: String){
def replaceBy(regex: Regex)(replace: Regex.MatchData => Option[String]): String = {
val sb = new StringBuilder()
var i = 0
regex.findAllIn(value).matchData.foreach { m =>
sb.append(value.substring(i, m.start))
i = m.end
replace(m) match {
case Some(s) => sb.append(s)
case None => sb.append(m.matched)
}
}
if(i < value.length){
sb.append(value.substring(i))
}
sb.toString
}
def toIntOpt: Option[Int] = try {
Option(Integer.parseInt(value))
} catch {
case e: NumberFormatException => None
}
}
// TODO Should this implicit conversion move to model.Functions?
implicit def extendsColumn(c1: Column[Boolean]) = new {
def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
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,18 +2,20 @@ 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
import org.eclipse.jgit.dircache.DirCacheEntry
/**
* Provides complex JGit operations.
@@ -42,8 +44,10 @@ object JGitUtil {
* @param message the last commit message
* @param commitId the last commit id
* @param committer the last committer name
* @param mailAddress the committer's mail address
*/
case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, time: Date, message: String, commitId: String, committer: String)
case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, time: Date, message: String, commitId: String,
committer: String, mailAddress: String)
/**
* The commit data.
@@ -51,39 +55,34 @@ object JGitUtil {
* @param id the commit id
* @param time the commit time
* @param committer the committer name
* @param mailAddress the mail address of the committer
* @param shortMessage the short message
* @param fullMessage the full message
* @param parents the list of parent commit id
*/
case class CommitInfo(id: String, time: Date, committer: String, shortMessage: String, fullMessage: String, parents: List[String]){
case class CommitInfo(id: String, time: Date, committer: String, mailAddress: String,
shortMessage: String, fullMessage: String, parents: List[String]){
def this(rev: org.eclipse.jgit.revwalk.RevCommit) = this(
rev.getName, rev.getCommitterIdent.getWhen, rev.getCommitterIdent.getName, rev.getShortMessage, rev.getFullMessage,
rev.getName,
rev.getCommitterIdent.getWhen,
rev.getCommitterIdent.getName,
rev.getCommitterIdent.getEmailAddress,
rev.getShortMessage,
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")
val description = defining(fullMessage.trim.indexOf("\n")){ i =>
if(i >= 0){
Some(fullMessage.trim.substring(i).trim)
} else {
None
}
} else None
}
}
case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String])
@@ -106,33 +105,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
}
@@ -141,29 +125,31 @@ 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 =>
// get commit count
val i = git.log.all.call.iterator
var commitCount = 0
while(i.hasNext && commitCount <= 1000){
i.next
commitCount = commitCount + 1
}
using(Git.open(getRepositoryDir(owner, repository))){ git =>
try {
// get commit count
val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(1000).sum
RepositoryInfo(
owner, repository, baseUrl + "/git/%s/%s.git".format(owner, repository),
// commit count
commitCount,
// branches
git.branchList.call.asScala.map { ref =>
ref.getName.replaceFirst("^refs/heads/", "")
}.toList,
// tags
git.tagList.call.asScala.map { ref =>
val revCommit = getRevCommitFromId(git, ref.getObjectId)
TagInfo(ref.getName.replaceFirst("^refs/tags/", ""), revCommit.getCommitterIdent.getWhen, revCommit.getName)
}.toList
)
RepositoryInfo(
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git",
// commit count
commitCount,
// branches
git.branchList.call.asScala.map { ref =>
ref.getName.replaceFirst("^refs/heads/", "")
}.toList,
// tags
git.tagList.call.asScala.map { ref =>
val revCommit = getRevCommitFromId(git, ref.getObjectId)
TagInfo(ref.getName.replaceFirst("^refs/tags/", ""), revCommit.getCommitterIdent.getWhen, revCommit.getName)
}.toList
)
} catch {
// not initialized
case e: NoHeadException => RepositoryInfo(
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", 0, Nil, Nil)
}
}
}
@@ -176,45 +162,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) =>
@@ -225,7 +209,8 @@ object JGitUtil {
commits(path).getCommitterIdent.getWhen,
commits(path).getShortMessage,
commits(path).getName,
commits(path).getCommitterIdent.getName)
commits(path).getCommitterIdent.getName,
commits(path).getCommitterIdent.getEmailAddress)
}.sortWith { (file1, file2) =>
(file1.isDirectory, file2.isDirectory) match {
case (true , false) => true
@@ -243,9 +228,9 @@ 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 = ""): (List[CommitInfo], Boolean) = {
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
@scala.annotation.tailrec
@@ -258,22 +243,48 @@ object JGitUtil {
case _ => (logs, i.hasNext)
}
val revWalk = new RevWalk(git.getRepository)
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(revision)))
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
})
}
Right(getCommitLog(revWalk.iterator, 0, Nil))
}
override def clone(): RevFilter = this
})
}
}
val commits = getCommitLog(revWalk.iterator, 0, Nil)
revWalk.release
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.
@@ -283,30 +294,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.
@@ -328,51 +318,11 @@ object JGitUtil {
* @return the list of latest commit
*/
def getLatestCommitFromPaths(git: Git, paths: List[String], revision: String): Map[String, RevCommit] = {
val map = new scala.collection.mutable.HashMap[String, RevCommit]
val revWalk = new RevWalk(git.getRepository)
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(revision)))
//revWalk.sort(RevSort.REVERSE);
val i = revWalk.iterator
while(i.hasNext && map.size != paths.length){
val commit = i.next
if(commit.getParentCount == 0){
// Initial commit
val treeWalk = new TreeWalk(git.getRepository)
treeWalk.reset()
treeWalk.setRecursive(true)
treeWalk.addTree(commit.getTree)
while (treeWalk.next) {
paths.foreach { path =>
if(treeWalk.getPathString.startsWith(path) && !map.contains(path)){
map.put(path, commit)
}
}
}
treeWalk.release
} else {
(0 to commit.getParentCount - 1).foreach { i =>
val parent = revWalk.parseCommit(commit.getParent(i).getId())
val df = new DiffFormatter(DisabledOutputStream.INSTANCE)
df.setRepository(git.getRepository)
df.setDiffComparator(RawTextComparator.DEFAULT)
df.setDetectRenames(true)
val diffs = df.scan(parent.getTree(), commit.getTree)
diffs.asScala.foreach { diff =>
paths.foreach { path =>
if(diff.getChangeType != ChangeType.DELETE && diff.getNewPath.startsWith(path) && !map.contains(path)){
map.put(path, commit)
}
}
}
}
}
revWalk.release
}
map.toMap
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)
}.toMap
}
/**
@@ -388,110 +338,160 @@ 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 =
using(new RepositoryBuilder().setGitDir(dir).setBare.build){ repository =>
repository.create
setReceivePack(repository)
}
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 =
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(
Some(if(revstr.isEmpty) repository.repository.defaultBranch else revstr),
repository.branchList.headOption
).flatMap {
case Some(rev) => Some((git.getRepository.resolve(rev), rev))
case None => None
}.find(_._1 != null)
}
}
def createDirCacheEntry(path: String, mode: FileMode, objectId: ObjectId): DirCacheEntry = {
val entry = new DirCacheEntry(path)
entry.setFileMode(mode)
entry.setObjectId(objectId)
entry
}
def createNewCommit(git: Git, inserter: ObjectInserter, headId: AnyObjectId, treeId: AnyObjectId,
fullName: String, mailAddress: String, message: String): String = {
val newCommit = new CommitBuilder()
newCommit.setCommitter(new PersonIdent(fullName, mailAddress))
newCommit.setAuthor(new PersonIdent(fullName, mailAddress))
newCommit.setMessage(message)
if(headId != null){
newCommit.setParentIds(List(headId).asJava)
}
newCommit.setTreeId(treeId)
val newHeadId = inserter.insert(newCommit)
inserter.flush()
inserter.release()
val refUpdate = git.getRepository.updateRef(Constants.HEAD)
refUpdate.setNewObjectId(newHeadId)
refUpdate.update()
newHeadId.getName
}
}

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,142 @@
package util
import util.ControlUtil._
import service.SystemSettingsService
import com.novell.ldap._
import java.security.Security
import org.slf4j.LoggerFactory
import service.SystemSettingsService.Ldap
import scala.annotation.tailrec
/**
* Utility for LDAP authentication.
*/
object LDAPUtil {
private val LDAP_VERSION: Int = LDAPConnection.LDAP_V3
private val logger = LoggerFactory.getLogger(getClass().getName())
/**
* Try authentication by LDAP using given configuration.
* Returns Right(LDAPUserInfo) if authentication is successful, otherwise Left(errorMessage).
*/
def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, LDAPUserInfo] = {
bind(
host = ldapSettings.host,
port = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
dn = ldapSettings.bindDN.getOrElse(""),
password = ldapSettings.bindPassword.getOrElse(""),
tls = ldapSettings.tls.getOrElse(false),
keystore = ldapSettings.keystore.getOrElse(""),
error = "System LDAP authentication failed."
){ conn =>
findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match {
case Some(userDN) => userAuthentication(ldapSettings, userDN, userName, password)
case None => Left("User does not exist.")
}
}
}
private def userAuthentication(ldapSettings: Ldap, userDN: String, userName: String, password: String): Either[String, LDAPUserInfo] = {
bind(
host = ldapSettings.host,
port = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
dn = userDN,
password = password,
tls = ldapSettings.tls.getOrElse(false),
keystore = ldapSettings.keystore.getOrElse(""),
error = "User LDAP Authentication Failed."
){ conn =>
findMailAddress(conn, userDN, ldapSettings.mailAttribute) match {
case Some(mailAddress) => Right(LDAPUserInfo(
userName = userName,
fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute =>
findFullName(conn, userDN, fullNameAttribute)
}.getOrElse(userName),
mailAddress = mailAddress))
case None => Left("Can't find mail address.")
}
}
}
private def bind[A](host: String, port: Int, dn: String, password: String, tls: Boolean, keystore: String, error: String)
(f: LDAPConnection => Either[String, A]): Either[String, A] = {
if (tls) {
// Dynamically set Sun as the security provider
Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider())
if (keystore.compareTo("") != 0) {
// Dynamically set the property that JSSE uses to identify
// the keystore that holds trusted root certificates
System.setProperty("javax.net.ssl.trustStore", keystore)
}
}
val conn: LDAPConnection = new LDAPConnection(new LDAPJSSEStartTLSFactory())
try {
// Connect to the server
conn.connect(host, port)
if (tls) {
// Secure the connection
conn.startTLS()
}
// Bind to the server
conn.bind(LDAP_VERSION, dn, password.getBytes)
// Execute a given function and returns a its result
f(conn)
} catch {
case e: Exception => {
// Provide more information if something goes wrong
logger.info("" + e)
if (conn.isConnected) {
conn.disconnect()
}
// Returns an error message
Left(error)
}
}
}
/**
* Search a specified user and returns userDN if exists.
*/
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 =>
if(results.hasMore) {
Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
} else None
}
private def findFullName(conn: LDAPConnection, userDN: String, nameAttribute: String): Option[String] =
defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](nameAttribute), false)){ results =>
if(results.hasMore) {
Option(results.next.getAttribute(nameAttribute)).map(_.getStringValue)
} else None
}
case class LDAPUserInfo(userName: String, fullName: String, mailAddress: String)
}

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,116 @@
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
import _root_.util.ControlUtil.defining
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 {
// TODO Can we use the Database Session in other than Transaction Filter?
database withSession {
getIssue(r.owner, r.name, issueId.toString) foreach { issue =>
defining(
s"[${r.name}] ${issue.title} (#${issueId})" ->
msg(view.Markdown.toHtml(content, r, false, true))) { case (subject, msg) =>
recipients(issue) { to =>
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)
}
smtp.fromAddress
.map (_ -> smtp.fromName.orNull)
.orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName))
.foreach { case (address, name) =>
email.setFrom(address, name)
}
email.setCharset("UTF-8")
email.setSubject(subject)
email.setHtmlMsg(msg)
email.addTo(to).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,11 +1,48 @@
package util
import java.net.{URLDecoder, URLEncoder}
import org.mozilla.universalchardet.UniversalDetector
import util.ControlUtil._
import org.apache.commons.io.input.BOMInputStream
import org.apache.commons.io.IOUtils
object StringUtil {
def encrypt(value: String): String = {
val md = java.security.MessageDigest.getInstance("SHA-1")
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")
md.update(value.getBytes)
md.digest.map(b => "%02x".format(b)).mkString
}
def urlEncode(value: String): String = URLEncoder.encode(value, "UTF-8")
def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8")
def splitWords(value: String): Array[String] = value.split("[ \\t ]+")
def escapeHtml(value: String): String =
value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
/**
* Make string from byte array. Character encoding is detected automatically by [[util.StringUtil.detectEncoding]].
* And if given bytes contains UTF-8 BOM, it's removed from returned string..
*/
def convertFromByteArray(content: Array[Byte]): String =
IOUtils.toString(new BOMInputStream(new java.io.ByteArrayInputStream(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

@@ -1,7 +1,7 @@
package util
import jp.sf.amateras.scalatra.forms._
import scala.Some
import org.scalatra.i18n.Messages
trait Validations {
@@ -9,11 +9,11 @@ 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] =
if(!value.matches("^[a-zA-Z0-9\\-_]+$")){
Some("%s contains invalid character.".format(name))
override def validate(name: String, value: String, messages: Messages): Option[String] =
if(!value.matches("^[a-zA-Z0-9\\-_.]+$")){
Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){
Some("%s starts with invalid character.".format(name))
Some(s"${name} starts with invalid character.")
} else {
None
}
@@ -26,10 +26,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, messages: Messages): java.util.Date = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(value)
}
}

View File

@@ -0,0 +1,51 @@
package view
import service.RequestCache
import twirl.api.Html
import util.StringUtil
trait AvatarImageProvider { self: RequestCache =>
/**
* Returns &lt;img&gt; which displays the avatar icon.
* Looks up Gravatar if avatar icon has not been configured in user settings.
*/
protected def getAvatarImageHtml(userName: String, size: Int,
mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html = {
val src = if(mailAddress.isEmpty){
// by user name
getAccountByUserName(userName).map { account =>
if(account.image.isEmpty && getSystemSettings().gravatar){
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}"""
} else {
s"""${context.path}/${account.userName}/_avatar"""
}
} getOrElse {
s"""${context.path}/_unknown/_avatar"""
}
} else {
// by mail address
getAccountByMailAddress(mailAddress).map { account =>
if(account.image.isEmpty && getSystemSettings().gravatar){
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}"""
} else {
s"""${context.path}/${account.userName}/_avatar"""
}
} getOrElse {
if(getSystemSettings().gravatar){
s"""https://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}"""
} else {
s"""${context.path}/_unknown/_avatar"""
}
}
}
if(tooltip){
Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title="${userName}"/>""")
} else {
Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" />""")
}
}
}

View File

@@ -0,0 +1,34 @@
package view
import service.RequestCache
import util.Implicits.RichString
trait LinkConverter { self: RequestCache =>
/**
* Converts issue id, username and commit id to link.
*/
protected def convertRefsLinks(value: String, repository: service.RepositoryService.RepositoryInfo,
issueIdPrefix: String = "#")(implicit context: app.Context): String = {
value
// escape HTML tags
.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;")
// convert issue id to link
.replaceBy(("(?<=(^|\\W))" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r){ m =>
getIssue(repository.owner, repository.name, m.group(2)) match {
case Some(issue) if(issue.isPullRequest)
=> Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/pull/${m.group(2)}">#${m.group(2)}</a>""")
case Some(_) => Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/issues/${m.group(2)}">#${m.group(2)}</a>""")
case None => Some(s"""#${m.group(2)}""")
}
}
// convert @username to link
.replaceBy("(?<=(^|\\W))@([a-zA-Z0-9\\-_]+)(?=(\\W|$))".r){ m =>
getAccountByUserName(m.group(2)).map { _ =>
s"""<a href="${context.path}/${m.group(2)}">@${m.group(2)}</a>"""
}
}
// convert commit id to link
.replaceAll("(?<=(^|\\W))([a-f0-9]{40})(?=(\\W|$))", s"""<a href="${context.path}/${repository.owner}/${repository.name}/commit/$$2">$$2</a>""")
}
}

View File

@@ -1,10 +1,14 @@
package view
import util.StringUtil
import util.ControlUtil._
import util.Directory._
import org.parboiled.common.StringUtils
import org.pegdown._
import org.pegdown.ast._
import org.pegdown.LinkRenderer.Rendering
import scala.collection.JavaConverters._
import service.{RequestCache, WikiService}
object Markdown {
@@ -12,17 +16,22 @@ object Markdown {
* Converts Markdown of Wiki pages to HTML.
*/
def toHtml(markdown: String, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableCommitLink: Boolean, enableIssueLink: Boolean)(implicit context: app.Context): String = {
val rootNode = new PegDownProcessor(
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES
).parseMarkdown(markdown.toCharArray)
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): String = {
// escape issue id
val source = if(enableRefsLink){
markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2")
} else markdown
new GitBucketHtmlSerializer(markdown, context, repository, enableWikiLink, enableCommitLink, enableIssueLink).toHtml(rootNode)
val rootNode = new PegDownProcessor(
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS
).parseMarkdown(source.toCharArray)
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink).toHtml(rootNode)
}
}
class GitBucketLinkRender(context: app.Context, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean) extends LinkRenderer {
enableWikiLink: Boolean) extends LinkRenderer with WikiService {
override def render(node: WikiLinkNode): Rendering = {
if(enableWikiLink){
try {
@@ -33,11 +42,16 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
} else {
(text, text)
}
val url = repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") +
"/wiki/" + java.net.URLEncoder.encode(page.replace(' ', '-'), "UTF-8")
new Rendering(url, label)
val url = repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/" + StringUtil.urlEncode(page)
if(getWikiPage(repository.owner, repository.name, page).isDefined){
new Rendering(url, label)
} else {
new Rendering(url, label).withAttribute("class", "absent")
}
} catch {
case e: java.io.UnsupportedEncodingException => throw new IllegalStateException();
case e: java.io.UnsupportedEncodingException => throw new IllegalStateException
}
} else {
super.render(node)
@@ -46,7 +60,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('"')
@@ -64,15 +78,13 @@ class GitBucketVerbatimSerializer extends VerbatimSerializer {
class GitBucketHtmlSerializer(
markdown: String,
context: app.Context,
repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
enableCommitLink: Boolean,
enableIssueLink: Boolean
) extends ToHtmlSerializer(
enableRefsLink: Boolean
)(implicit val context: app.Context) extends ToHtmlSerializer(
new GitBucketLinkRender(context, repository, enableWikiLink),
Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava
) {
) with LinkConverter with RequestCache {
override protected def printImageTag(imageNode: SuperNode, url: String): Unit =
printer.print("<img src=\"").print(fixUrl(url)).print("\" alt=\"").printEncoded(printChildrenToString(imageNode)).print("\"/>")
@@ -94,15 +106,13 @@ 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) {
// convert commit id to link.
val text = if(enableCommitLink) node.getText.replaceAll("(^|\\W)([0-9a-f]{40})(\\W|$)",
"<a href=\"%s/%s/%s/commit/$2\">$2</a>".format(context.path, repository.owner, repository.name))
else node.getText
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
if (abbreviations.isEmpty) {
printer.print(text)
@@ -111,15 +121,4 @@ class GitBucketHtmlSerializer(
}
}
override def visit(node: HeaderNode) {
val text = markdown.substring(node.getStartIndex, node.getEndIndex - 1).trim
if(enableIssueLink && text.matches("#[\\d]+")){
// convert issue id to link
val issueId = text.substring(1).toInt
printer.print("<a href=\"%s/%s/%s/issues/%d\">#%d</a>".format(context.path, repository.owner, repository.name, issueId, issueId))
} else {
printTag(node, "h" + node.getLevel)
}
}
}
}

View File

@@ -2,17 +2,19 @@ package view
import java.util.Date
import java.text.SimpleDateFormat
import twirl.api.Html
import util.StringUtil
import service.RequestCache
/**
* Provides helper methods for Twirl templates.
*/
object helpers {
object helpers extends AvatarImageProvider with LinkConverter with RequestCache {
/**
* Format java.util.Date to "yyyy-MM-dd HH:mm:ss".
*/
def datetime(date: Date): String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date)
/**
* Format java.util.Date to "yyyy-MM-dd".
*/
@@ -29,54 +31,114 @@ object helpers {
* Converts Markdown of Wiki pages to HTML.
*/
def markdown(value: String, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableCommitLink: Boolean, enableIssueLink: Boolean)(implicit context: app.Context): Html = {
Html(Markdown.toHtml(value, repository, enableWikiLink, enableCommitLink, enableIssueLink))
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 for the given user name.
* This method looks up Gravatar if avatar icon has not been configured in user settings.
*/
def avatar(userName: String, size: Int, tooltip: Boolean = false)(implicit context: app.Context): Html =
getAvatarImageHtml(userName, size, "", tooltip)
/**
* Returns &lt;img&gt; which displays the avatar icon for the given mail address.
* This method looks up Gravatar if avatar icon has not been configured in user settings.
*/
def avatar(commit: util.JGitUtil.CommitInfo, size: Int)(implicit context: app.Context): Html =
getAvatarImageHtml(commit.committer, size, commit.mailAddress)
/**
* Converts commit id, issue id and username to the link.
*/
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
}
import scala.util.matching.Regex._
implicit class RegexReplaceString(s: String) {
def replaceAll(pattern: String, replacer: (Match) => String): String = {
pattern.r.replaceAllIn(s, replacer)
}
}
def activityMessage(message: String)(implicit context: app.Context): Html = {
val a = s"a $message aa $$1 a"
/**
* Convert link notations in the activity message.
*/
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>""")
.replaceAll("\\[user:([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1">$$1</a>""")
.replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
.replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
.replaceAll("\\[user:([^\\s]+?)\\]" , (m: Match) => user(m.group(1)).body)
)
}
/**
* URL encode except '/'.
*/
def encodeRefName(value: String): String = StringUtil.urlEncode(value).replace("%2F", "/")
def urlEncode(value: String): String = StringUtil.urlEncode(value)
def urlEncode(value: Option[String]): String = value.map(urlEncode).getOrElse("")
/**
* Generates the url to the repository.
*/
def url(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context): String =
"%s/%s/%s".format(context.path, repository.owner, repository.name)
s"${context.path}/${repository.owner}/${repository.name}"
/**
* Generates the url to the account page.
*/
def url(userName: String)(implicit context: app.Context): String = "%s/%s".format(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/assets".format(context.path)
def assets(implicit context: app.Context): String = s"${context.path}/assets"
/**
* Converts issue id and commit id to link.
* Generates the text link to the account page.
* If user does not exist or disabled, this method returns user name as text without link.
*/
def link(value: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context): Html =
Html(value
// escape HTML tags
.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;")
// convert issue id to link
.replaceAll("(^|\\W)#(\\d+)(\\W|$)", "$1<a href=\"%s/%s/%s/issues/$2\">#$2</a>$3".format(context.path, repository.owner, repository.name))
// convert commit id to link
.replaceAll("(^|\\W)([a-f0-9]{40})(\\W|$)", "$1<a href=\"%s/%s/%s/commit/$2\">$2</a>$3").format(context.path, repository.owner, repository.name))
def user(userName: String, mailAddress: String = "", styleClass: String = "")(implicit context: app.Context): Html =
userWithContent(userName, mailAddress, styleClass)(Html(userName))
/**
* Generates the avatar link to the account page.
* If user does not exist or disabled, this method returns avatar image without link.
*/
def avatarLink(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html =
userWithContent(userName, mailAddress)(avatar(userName, size, tooltip))
private def userWithContent(userName: String, mailAddress: String = "", styleClass: String = "")(content: Html)(implicit context: app.Context): Html =
(if(mailAddress.isEmpty){
getAccountByUserName(userName)
} else {
getAccountByMailAddress(mailAddress)
}).map { account =>
Html(s"""<a href="${url(account.userName)}" class="${styleClass}">${content}</a>""")
} getOrElse content
/**
* Test whether the given Date is past date.
*/
def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime
/**
* Implicit conversion to add mkHtml() to Seq[Html].
*/
implicit def extendsHtmlSeq(seq: Seq[Html]) = new {
implicit class RichHtmlSeq(seq: Seq[Html]) {
def mkHtml(separator: String) = Html(seq.mkString(separator))
def mkHtml(separator: scala.xml.Elem) = Html(seq.mkString(separator.toString))
}

View File

@@ -1,22 +1,6 @@
@(account: model.Account, activities: List[model.Activity])(implicit context: app.Context)
@(account: model.Account, groupNames: List[String], activities: List[model.Activity])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(account.userName){
<div class="container-fluid">
<div class="row-fluid">
<div class="span4">
<div class="block">
<div class="block-header">@account.userName</div>
</div>
<div class="block">
<div><i class="icon-home"></i> <a href="@account.url">@account.url</a></div>
<div><i class="icon-time"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
</div>
</div>
<div class="span8">
@tab(account, "activity")
@helper.html.activities(activities)
</div>
</div>
</div>
@main(account, groupNames, "activity"){
@helper.html.activities(activities)
}

View File

@@ -1,4 +1,4 @@
@(account: Option[model.Account])(implicit context: app.Context)
@(account: Option[model.Account], info: Option[Any])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main((if(account.isDefined) "Edit your profile" else "Create your account")){
@@ -7,35 +7,58 @@
} else {
<h3>Create your account</h3>
}
@helper.html.information(info)
<form action="@if(account.isDefined){@url(account.get.userName)/_edit}else{@path/register}" method="POST" validate="true">
@if(account.isEmpty){
<fieldset>
<label for="userName"><strong>User name</strong></label>
<input type="text" name="userName" id="userName" value=""/>
<span id="error-userName" class="error"></span>
</fieldset>
}
<fieldset>
<label for="password"><strong>Password</strong>
@if(account.nonEmpty){
(Input to change password)
<div class="row-fluid">
<div class="span6">
@if(account.isEmpty){
<fieldset>
<label for="userName" class="strong">Username:</label>
<input type="text" name="userName" id="userName" value=""/>
<span id="error-userName" class="error"></span>
</fieldset>
}
</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>
<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>
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
<span id="error-url" class="error"></span>
</fieldset>
<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="fullName" class="strong">Full Name:</label>
<input type="text" name="fullName" id="fullName" value="@account.map(_.fullName)"/>
<span id="error-fullName" class="error"></span>
</fieldset>
<fieldset>
<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" 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" class="strong">Image (optional):</label>
@helper.html.uploadavatar(account)
</fieldset>
</div>
</div>
<fieldset class="margin">
@if(account.isDefined){
<div class="pull-right">
<a href="@path/@account.get.userName/_delete" class="btn btn-danger" id="delete">Delete account</a>
</div>
<input type="submit" class="btn btn-success" value="Save"/>
<a href="@url(account.get.userName)" class="btn">Cancel</a>
} else {
@@ -44,3 +67,10 @@
</fieldset>
</form>
}
<script>
$(function(){
$('#delete').click(function(){
return confirm('Once you delete your account, there is no going back.\nAre you sure?');
});
});
</script>

View File

@@ -0,0 +1,49 @@
@(account: model.Account, groupNames: List[String], active: String)(body: Html)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(account.userName){
<div class="container-fluid">
<div class="row-fluid">
<div class="span4">
<div class="block">
<div class="account-image">@avatar(account.userName, 200)</div>
<div class="account-fullname">@account.fullName</div>
<div class="account-username">@account.userName</div>
</div>
<div class="block">
@if(account.url.isDefined){
<div><i class="icon-home"></i> <a href="@account.url">@account.url</a></div>
}
<div><i class="icon-time"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
</div>
@if(groupNames.nonEmpty){
<div>
<div>Groups</div>
@groupNames.map { groupName =>
<a href="@url(groupName)">@avatar(groupName, 36, tooltip = true)</a>
}
</div>
}
</div>
<div class="span8">
<ul class="nav nav-tabs">
<li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
@if(account.isGroupAccount){
<li@if(active == "members"){ class="active"}><a href="@url(account.userName)?tab=members">Members</a></li>
} else {
<li@if(active == "activity"){ class="active"}><a href="@url(account.userName)?tab=activity">Public Activity</a></li>
}
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
<li class="pull-right">
<div class="button-group">
<a href="@url(account.userName)/_edit" class="btn">Edit Your Profile</a>
</div>
</li>
}
</ul>
@body
</div>
</div>
</div>
}

View File

@@ -0,0 +1,16 @@
@(account: model.Account, members: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@main(account, Nil, "members"){
@if(members.isEmpty){
No members
} else {
@members.map { userName =>
<div class="block">
<div class="block-header">
@avatar(userName, 20) <a href="@url(userName)">@userName</a>
</div>
</div>
}
}
}

View File

@@ -1,41 +1,26 @@
@(account: model.Account, repositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
@(account: model.Account, groupNames: List[String], repositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(account.userName){
<div class="container-fluid">
<div class="row-fluid">
<div class="span4">
<div class="block">
<div class="block-header">@account.userName</div>
</div>
<div class="block">
<div><i class="icon-home"></i> <a href="@account.url">@account.url</a></div>
<div><i class="icon-time"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
</div>
</div>
<div class="span8">
@tab(account, "repositories")
@if(repositories.isEmpty){
No repositories
} else {
@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.description.isDefined){
<div>@repository.repository.description</div>
}
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
</div>
@main(account, groupNames, "repositories"){
@if(repositories.isEmpty){
No repositories
} else {
@repositories.map { repository =>
<div class="block">
<div class="block-header">
<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>
}
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
</div>
</div>
</div>
}
}
}

View File

@@ -1,14 +0,0 @@
@(account: model.Account, active: String)(implicit context: app.Context)
@import context._
@import view.helpers._
<ul class="nav nav-tabs">
<li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
<li@if(active == "activity"){ class="active"}><a href="@url(account.userName)?tab=activity">Public Activity</a></li>
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
<li class="pull-right">
<div class="button-group">
<a href="@url(account.userName)/_edit" class="btn">Edit Your Profile</a>
</div>
</li>
}
</ul>

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