Compare commits

...

364 Commits
1.5 ... 1.11

Author SHA1 Message Date
takezoe
0961eb5976 (refs #279)Fix redirect url generation. 2014-03-02 23:55:03 +09:00
takezoe
153244c390 Update README.mf for GitBucket 1.11 release 2014-03-01 15:26:10 +09:00
takezoe
e97b5c3c89 (refs #279)Remove --https option because it's possible to substitute in the base url configuration. 2014-03-01 15:24:55 +09:00
takezoe
374893a5ae Apply scala.util.control.Exception to exception handling. 2014-03-01 15:22:58 +09:00
takezoe
17f581f654 (refs #279)Override ScalatraBase#fullUrl() to apply the configured base url to redirection. 2014-03-01 15:19:42 +09:00
takezoe
590b431ec1 Add FlashMapSupport to ControllerBase. 2014-03-01 13:27:36 +09:00
takezoe
98266fe0e1 (refs #279)Fix webhook URL to use the configured base URL. 2014-03-01 13:05:42 +09:00
Naoki Takezoe
b620307983 Merge pull request #287 from lefou/correct-readme-name
Show the correct name of the readme file
2014-02-27 15:37:55 +09:00
Tobias Roeser
2c14dfb781 Show the correct name of the readme file (instead of showing always README.md). 2014-02-26 12:09:14 +01:00
Naoki Takezoe
1f71619b6b Merge pull request #281 from maliayas/patch-1
Apply GitHub diff colors
2014-02-24 12:15:57 +09:00
Naoki Takezoe
5b34b9c795 Update README.md 2014-02-24 02:34:13 +09:00
Naoki Takezoe
99d15899f6 Update README.md 2014-02-24 02:32:19 +09:00
takezoe
c114a8b507 (refs #279)Fix TestCase. 2014-02-24 02:16:18 +09:00
takezoe
0dd37c2481 (refs #279)Fix redirect URL generation in authentication. 2014-02-24 02:14:10 +09:00
Ali Ayas
b5d7c96bba Apply GitHub diff colors
However, jsdifflib is still not a good approach. It requires dumping all the two text to the browser and do the work there. Instead, maybe the diff should be taken from the git itself and highlighting should be applied on that.

OTOH, word level diff would be good when applicable (like GitHub does).
2014-02-23 13:11:10 +02:00
takezoe
a76792ced4 (refs #279)Add configuration to specify the base URL. But still one problem has not been resolved. 2014-02-22 14:20:51 +09:00
takezoe
39091240ff (refs #60)Mentioned issue reference from other issue or issue comment. 2014-02-22 05:13:39 +09:00
takezoe
0ccb753892 (refs #187)Add large icons for repository. 2014-02-22 02:00:49 +09:00
takezoe
63dda84c8b Add Version 1.11 2014-02-21 12:05:15 +09:00
takezoe
7ba1f85d48 (refs #187)Repository icons are updated. 2014-02-21 11:39:02 +09:00
takezoe
bb9a23fe0f Fix TestCase. 2014-02-20 03:40:06 +09:00
takezoe
8536824d7e (refs #231)Fix anchor icon style and apply URL encoding to non-ascii chars in anchor name. 2014-02-19 03:40:39 +09:00
takezoe
78073babe4 (refs #231)Add anchor icon for headlines in Markdown. 2014-02-19 03:19:34 +09:00
Naoki Takezoe
521d15219c Merge pull request #272 from jeffreyolchovy/issue/231
Fix for #231: Generate headline anchor in Wiki pages
2014-02-11 04:53:37 +09:00
Naoki Takezoe
7469a3c349 Merge pull request #270 from rndstr/patch-1
Update new data directory in README.md
2014-02-10 07:50:40 +09:00
Jeffrey Olchovy
153a32e340 (refs #231)Generate anchors for headers in wiki markup 2014-02-08 13:59:27 -05:00
Roland Schilter
f155d4f150 Update new data directory in README.md 2014-02-08 13:11:13 +01:00
takezoe
d683dd2c38 (refs #268)Fix label filter bug when label contains whitespaces. 2014-02-08 07:04:18 +09:00
takezoe
7ebba741a8 (refs #232)Highlight lines which are specified by URL hash. 2014-02-08 06:52:50 +09:00
takezoe
d10f683098 (refs #259)Add underline to h1 and h2 in div.markdown-body. 2014-02-08 05:14:26 +09:00
takezoe
0270133ecf (refs #267)Improve H2 connectivity 2014-02-08 05:06:24 +09:00
takezoe
d7b479d97d (refs #265)Fix label pulldown position. 2014-02-08 05:04:06 +09:00
takezoe
4366c512fe (refs #265)Label editing for the pull request. 2014-02-05 03:07:37 +09:00
Naoki Takezoe
229a773ed2 Merge pull request #262 from shootaroo/jump-line
Add id for line number
2014-02-04 09:41:24 -08:00
takezoe
d882f20436 (refs #254)Change comment action to "delete_branch" from "delete". 2014-02-04 17:20:48 +09:00
takezoe
9d7235af20 (refs #254)Store removed branch name into CONTENT column of COMMENT table. 2014-02-04 17:06:54 +09:00
takezoe
c2eb53d154 (refs #224)Add delete branch button to pull request from same repository. 2014-02-04 09:04:25 +09:00
takezoe
7629e347df (refs #224)Record delete branch activity 2014-02-03 08:06:04 +09:00
takezoe
2764caae29 (refs #224)Add delete branch button 2014-02-03 08:00:43 +09:00
Naoki Takezoe
a87bd2a928 Merge pull request #264 from bati11/257-https-reverse-proxy
Fix #257, "org.scalatra.ForceHttps" set to true, if --https=true
2014-02-01 21:39:01 -08:00
bati11
202c920064 Fix #257, "org.scalatra.ForceHttps" set to true, if --https=true
ScalatraBase.redirect() use "org.scalatra.ForceHttps" in servlet
context init parameter when choice 'http' or 'https'.
2014-02-02 03:17:17 +09:00
Naoki Takezoe
a08316bba0 Update README.md 2014-02-01 17:15:50 +09:00
Naoki Takezoe
520e5ebb7a Update README.md 2014-02-01 17:14:41 +09:00
Naoki Takezoe
5d5a4cacb1 Update README.md 2014-02-01 17:13:18 +09:00
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
shootaroo
05a91565dc Add id for line number 2014-01-30 15:16:29 +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
38df990033 Merge branch 'master' into webhook 2013-09-05 02:35:58 +09:00
takezoe
940e2f4759 (refs #74)Add WEB_HOOK table. 2013-09-02 18:17:28 +09:00
takezoe
6fe65c76b1 (refs #74)Add the web hook configuration page. 2013-08-28 13:21:51 +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
828688ddd0 (refs #33)Match committer by mail address. 2013-07-12 04:27:20 +09:00
158 changed files with 5621 additions and 2394 deletions

1
.gitignore vendored
View File

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

147
README.md
View File

@@ -6,7 +6,7 @@ GitBucket is the easily installable Github clone written with Scala.
The current version of GitBucket provides a basic features below: The current version of GitBucket provides a basic features below:
- Public / Private Git repository (http access only) - 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) - Repository search (Code and Issues)
- Wiki - Wiki
- Issues - Issues
@@ -15,9 +15,13 @@ The current version of GitBucket provides a basic features below:
- Activity timeline - Activity timeline
- User management (for Administrators) - User management (for Administrators)
- Group (like Organization in Github) - Group (like Organization in Github)
- LDAP integration
- Gravatar support
Following features are not implemented, but we will make them in the future release! Following features are not implemented, but we will make them in the future release!
- File editing in repository viewer
- Comment for the changeset
- Network graph - Network graph
- Statics - Statics
- Watch / Star - Watch / Star
@@ -28,55 +32,128 @@ Installation
-------- --------
1. Download latest **gitbucket.war** from [the release page](https://github.com/takezoe/gitbucket/releases). 1. Download latest **gitbucket.war** from [the release page](https://github.com/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. 3. Access **http://[hostname]:[port]/gitbucket/** using your web browser.
The default administrator account is **root** and password is **root**. The default administrator account is **root** and password is **root**.
To upgrade GitBucket, only replace gitbucket.war. 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]
- --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 Release Notes
-------- --------
### 1.11 - 01 Mar 2014
- Base URL for redirection, notification and repository URL box is configurable
- Remove ```--https``` option because it's possible to substitute in the base url
- Headline anchor is available for Markdown contents such as Wiki page
- Improve H2 connectivity
- Label is available for pull requests not only issues
- Delete branch button is added
- Repository icons are updated
- Select lines of source code by URL hash like `#L10` or `#L10-L15` in repository viewer
- Display reference to issue from others in comment list
- Fix some bugs
### 1.10 - 01 Feb 2014
- Rename repository
- Transfer repository owner
- Change default data directory to `HOME/.gitbucket` from `HOME/gitbucket` to avoid problem like #243, but if data directory already exist at HOME/gitbucket, it continues being used.
- Add LDAP display name attribute
- Response performance improvement
- Fix some bugs
### 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 ### 1.5 - 4 Sep 2013
- Fork and pull request. - Fork and pull request
- LDAP authentication. - LDAP authentication
- Mail notification. - Mail notification
- Add an option to turn off the gravatar support. - Add an option to turn off the gravatar support
- Add the branch tab in the repository viewer. - Add the branch tab in the repository viewer
- Encoding auto detection for the file content in the repository viewer. - Encoding auto detection for the file content in the repository viewer
- Add favicon, header logo and icons for the timeline. - Add favicon, header logo and icons for the timeline
- Specify data directory via environment variable GITBUCKET_HOME. - Specify data directory via environment variable GITBUCKET_HOME
- Fixed some bugs. - Fix some bugs
### 1.4 - 31 Jul 2013 ### 1.4 - 31 Jul 2013
- Group management. - Group management
- Repository search for code and issues. - Repository search for code and issues
- Display user related issues on the dashboard. - Display user related issues on the dashboard
- Display participants avatar of issues on the issue page. - Display participants avatar of issues on the issue page
- Performance improvement for repository viewer. - Performance improvement for repository viewer
- Alert by milestone due date. - Alert by milestone due date
- H2 database administration console. - H2 database administration console
- Fixed some bugs. - Fix some bugs
### 1.3 - 18 Jul 2013 ### 1.3 - 18 Jul 2013
- Batch updating for issues. - Batch updating for issues
- Display assigned user on issue list. - Display assigned user on issue list
- User icon and Gravatar support. - User icon and Gravatar support
- Convert @xxxx to link to the account page. - Convert @xxxx to link to the account page
- Add copy to clipboard button for git clone URL. - Add copy to clipboard button for git clone URL
- Allows multi-byte characters as wiki page name. - Allow multi-byte characters as wiki page name
- Allows to create the empty repository. - Allow to create the empty repository
- Fixed some bugs. - Fix some bugs
### 1.2 - 09 Jul 2013 ### 1.2 - 09 Jul 2013
- Added activity timeline. - Add activity timeline
- Bugfix for Git 1.8.1.5 or later. - Bugfix for Git 1.8.1.5 or later
- Allows multi-byte characters as label. - Allow multi-byte characters as label
- Fixed some bugs. - Fix some bugs
### 1.1 - 05 Jul 2013 ### 1.1 - 05 Jul 2013
- Fixed some bugs. - Fix some bugs
- Upgrade to JGit 3.0. - Upgrade to JGit 3.0
### 1.0 - 04 Jul 2013 ### 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

@@ -25,17 +25,17 @@
inkscape:pageopacity="0.0" inkscape:pageopacity="0.0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:zoom="1.4" inkscape:zoom="1.4"
inkscape:cx="629.30023" inkscape:cx="450.21999"
inkscape:cy="281.44758" inkscape:cy="97.51519"
inkscape:document-units="px" inkscape:document-units="px"
inkscape:current-layer="layer1-9" inkscape:current-layer="layer1-9"
showgrid="false" showgrid="false"
inkscape:window-width="1366" inkscape:window-width="1366"
inkscape:window-height="705" inkscape:window-height="706"
inkscape:window-x="-8" inkscape:window-x="1912"
inkscape:window-y="-8" inkscape:window-y="-8"
inkscape:window-maximized="1" inkscape:window-maximized="1"
inkscape:snap-global="true" inkscape:snap-global="false"
inkscape:snap-grids="false" inkscape:snap-grids="false"
inkscape:snap-page="false" inkscape:snap-page="false"
inkscape:snap-bbox="true" inkscape:snap-bbox="true"
@@ -746,6 +746,238 @@
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" 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" id="rect2995-0-2-7-7"
inkscape:connector-curvature="0" /> inkscape:connector-curvature="0" />
<rect
style="color:#000000;fill:none;stroke:#b3b3b3;stroke-width:9.34194565;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="rect3083"
width="170.93134"
height="207.72536"
x="38.526306"
y="1299.8645" />
<rect
style="color:#000000;fill:none;stroke:#b3b3b3;stroke-width:8.41239071;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="rect3083-7"
width="171.86089"
height="167.53221"
x="38.061527"
y="1300.4821" />
<rect
id="rect2995-0-4"
y="1301.3412"
x="42.553577"
height="163.64935"
width="29.769083"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-0"
y="1321.9025"
x="85.732407"
height="17.555511"
width="16.782965"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-0-9"
y="1356.7848"
x="85.732407"
height="17.555511"
width="16.782965"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-0-9-4"
y="1391.6671"
x="85.732407"
height="17.555511"
width="16.782965"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-0-9-4-8"
y="1426.5494"
x="85.732407"
height="17.555511"
width="16.782965"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-8"
y="1482.7141"
x="70.149086"
height="30.541632"
width="42.755199"
style="fill:#b3b3b3;stroke:none" />
<path
sodipodi:type="star"
style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:7.55999994;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="path4002"
sodipodi:sides="3"
sodipodi:cx="235.71429"
sodipodi:cy="1000.2193"
sodipodi:r1="15.016997"
sodipodi:r2="7.5084987"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 250.73129,1000.2193 -11.26275,6.5026 -11.26274,6.5025 0,-13.0051 0,-13.0051 11.26274,6.50255 z"
transform="matrix(1.0346242,0,0,1.5150471,-165.95814,-2.7851671)"
inkscape:transform-center-x="-2.5637799" />
<path
sodipodi:type="star"
style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:7.55999994;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="path4002-2"
sodipodi:sides="3"
sodipodi:cx="235.71429"
sodipodi:cy="1000.2193"
sodipodi:r1="15.016997"
sodipodi:r2="7.5084987"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 250.73129,1000.2193 -11.26275,6.5026 -11.26274,6.5025 0,-13.0051 0,-13.0051 11.26274,6.50255 z"
transform="matrix(-0.93510984,0,0,1.5150471,326.24502,-2.7851671)"
inkscape:transform-center-x="3.5106467" />
<rect
style="color:#000000;fill:none;stroke:#b3b3b3;stroke-width:9.34194565;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="rect3083-4"
width="170.93134"
height="207.72536"
x="280.50113"
y="1299.152" />
<rect
style="color:#000000;fill:none;stroke:#b3b3b3;stroke-width:8.41239071;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="rect3083-7-5"
width="171.86087"
height="167.53221"
x="280.03638"
y="1299.7695" />
<rect
id="rect2995-0-4-5"
y="1300.6287"
x="284.52841"
height="163.64934"
width="29.769083"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-8-5"
y="1482.0016"
x="312.12393"
height="30.541632"
width="42.755199"
style="fill:#b3b3b3;stroke:none" />
<path
sodipodi:type="star"
style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:7.55999994;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="path4002-27"
sodipodi:sides="3"
sodipodi:cx="235.71429"
sodipodi:cy="1000.2193"
sodipodi:r1="15.016997"
sodipodi:r2="7.5084987"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 250.73129,1000.2193 -11.26275,6.5026 -11.26274,6.5025 0,-13.0051 0,-13.0051 11.26274,6.50255 z"
transform="matrix(1.0346242,0,0,1.5150471,76.016678,-3.496726)"
inkscape:transform-center-x="-3.8842459"
inkscape:transform-center-y="-1.5464308e-005" />
<path
sodipodi:type="star"
style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:7.55999994;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="path4002-2-6"
sodipodi:sides="3"
sodipodi:cx="235.71429"
sodipodi:cy="1000.2193"
sodipodi:r1="15.016997"
sodipodi:r2="7.5084987"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 250.73129,1000.2193 -11.26275,6.5026 -11.26274,6.5025 0,-13.0051 0,-13.0051 11.26274,6.50255 z"
transform="matrix(-0.93510984,0,0,1.5150471,568.21986,-3.496726)"
inkscape:transform-center-x="5.318797"
inkscape:transform-center-y="-1.5464308e-005" />
<rect
id="rect2995-0-4-5-7"
y="1392.2405"
x="365.67133"
height="58.049755"
width="29.769083"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-5-7-6"
y="1319.5453"
x="326.67615"
height="49.632401"
width="29.769083"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-5-7-8"
y="1179.0293"
x="-767.54126"
height="58.049755"
width="29.769083"
style="fill:#b3b3b3;stroke:none"
transform="matrix(0.68860063,-0.7251408,0.7251408,0.68860063,0,0)" />
<rect
id="rect2995-0-4-5-7-6-9"
y="1319.5453"
x="403.28595"
height="49.632404"
width="29.769083"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-5-7-8-2"
y="623.14606"
x="-1287.8975"
height="55.681484"
width="28.564859"
style="fill:#b3b3b3;stroke:none"
transform="matrix(-0.68607628,-0.72752961,-0.72274236,0.69111755,0,0)" />
<rect
style="color:#000000;fill:#ffffff;stroke:#b3b3b3;stroke-width:7.29121827999999980;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;fill-opacity:1"
id="rect3083-7-5-7"
width="172.98204"
height="125.03616"
x="529.78156"
y="1383.6165" />
<rect
id="rect2995-0-4-5-9"
y="1385.3533"
x="663.37042"
height="123.85819"
width="38.18644"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-5-9-5"
y="1401.4539"
x="552.03174"
height="15.96297"
width="117.00352"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-5-9-5-4"
y="1437.4023"
x="551.16083"
height="15.96297"
width="117.00352"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-5-9-5-4-3"
y="1473.7642"
x="551.16083"
height="15.96297"
width="117.00352"
style="fill:#b3b3b3;stroke:none" />
<path
style="fill:none;stroke:#b3b3b3;stroke-width:23.0681076;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 558.62308,1380.7989 0,-45.237 c 0,0 13.52904,-35.6384 56.38304,-36.1894 40.81922,-0.5248 55.47363,34.6931 55.47363,34.6931 l 0.17276,48.4719"
id="path4310"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccscc" />
</g> </g>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 47 KiB

1
project/build.properties Normal file
View File

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

View File

@@ -1,8 +1,6 @@
import sbt._ import sbt._
import Keys._ import Keys._
import org.scalatra.sbt._ import org.scalatra.sbt._
import org.scalatra.sbt.PluginKeys._
import sbt.ScalaVersion
import twirl.sbt.TwirlPlugin._ import twirl.sbt.TwirlPlugin._
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
@@ -10,7 +8,7 @@ object MyBuild extends Build {
val Organization = "jp.sf.amateras" val Organization = "jp.sf.amateras"
val Name = "gitbucket" val Name = "gitbucket"
val Version = "0.0.1" val Version = "0.0.1"
val ScalaVersion = "2.10.1" val ScalaVersion = "2.10.3"
val ScalatraVersion = "2.2.1" val ScalatraVersion = "2.2.1"
lazy val project = Project ( lazy val project = Project (
@@ -25,25 +23,31 @@ object MyBuild extends Build {
Classpaths.typesafeReleases, Classpaths.typesafeReleases,
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/" "amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
), ),
scalacOptions := Seq("-deprecation", "-language:postfixOps"),
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r", "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r",
"org.scalatra" %% "scalatra" % ScalatraVersion, "org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test", "org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
"org.scalatra" %% "scalatra-json" % ScalatraVersion, "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.2", "jp.sf.amateras" %% "scalatra-forms" % "0.0.11",
"commons-io" % "commons-io" % "2.4", "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-compress" % "1.5",
"org.apache.commons" % "commons-email" % "1.3.1", "org.apache.commons" % "commons-email" % "1.3.1",
"org.apache.httpcomponents" % "httpclient" % "4.3",
"com.typesafe.slick" %% "slick" % "1.0.1", "com.typesafe.slick" %% "slick" % "1.0.1",
"com.novell.ldap" % "jldap" % "2009-10-07", "com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.3.171", "com.h2database" % "h2" % "1.3.173",
"ch.qos.logback" % "logback-classic" % "1.0.6" % "runtime", "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container", "org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar")) "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: _*) ) ++ seq(Twirl.settings: _*)
) )
} }

View File

@@ -5,3 +5,5 @@ addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.1")
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.0") addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.0")
addSbtPlugin("io.spray" % "sbt-twirl" % "0.6.1") 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 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" %*

2
sbt.sh
View File

@@ -1 +1 @@
java -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar `dirname $0`/sbt-launch-0.12.3.jar "$@" 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,62 @@
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("--gitbucket.home")){
System.setProperty("gitbucket.home", dim[1]);
}
}
}
}
Server server = new Server();
SelectChannelConnector connector = new SelectChannelConnector();
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());
if (forceHttps) {
context.setInitParameter("org.scalatra.ForceHttps", "true");
}
server.setHandler(context);
server.start();
server.join();
}
}

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"?> <?xml version="1.0" encoding="UTF-8"?>
<configuration> <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> </configuration>

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,13 +1,22 @@
import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter}
import app._ import app._
import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
import org.scalatra._ import org.scalatra._
import javax.servlet._ import javax.servlet._
import java.util.EnumSet
class ScalatraBootstrap extends LifeCycle { class ScalatraBootstrap extends LifeCycle {
override def init(context: ServletContext) { 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 IndexController, "/")
context.mount(new SearchController, "/") context.mount(new SearchController, "/")
context.mount(new FileUploadController, "/upload") context.mount(new FileUploadController, "/upload")
context.mount(new SignInController, "/*")
context.mount(new DashboardController, "/*") context.mount(new DashboardController, "/*")
context.mount(new UserManagementController, "/*") context.mount(new UserManagementController, "/*")
context.mount(new SystemSettingsController, "/*") context.mount(new SystemSettingsController, "/*")
@@ -20,7 +29,9 @@ class ScalatraBootstrap extends LifeCycle {
context.mount(new IssuesController, "/*") context.mount(new IssuesController, "/*")
context.mount(new PullRequestsController, "/*") context.mount(new PullRequestsController, "/*")
context.mount(new RepositorySettingsController, "/*") 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) val dir = new java.io.File(_root_.util.Directory.GitBucketHome)
if(!dir.exists){ if(!dir.exists){
dir.mkdirs() dir.mkdirs()

View File

@@ -5,25 +5,24 @@ import util.{FileUtil, OneselfAuthenticator}
import util.StringUtil._ import util.StringUtil._
import util.Directory._ import util.Directory._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.scalatra.FlashMapSupport import org.apache.commons.io.FileUtils
class AccountController extends AccountControllerBase class AccountController extends AccountControllerBase
with SystemSettingsService with AccountService with RepositoryService with ActivityService with AccountService with RepositoryService with ActivityService with OneselfAuthenticator
with OneselfAuthenticator
trait AccountControllerBase extends AccountManagementControllerBase with FlashMapSupport { trait AccountControllerBase extends AccountManagementControllerBase {
self: SystemSettingsService with AccountService with RepositoryService with ActivityService self: AccountService with RepositoryService with ActivityService with OneselfAuthenticator =>
with OneselfAuthenticator =>
case class AccountNewForm(userName: String, password: String,mailAddress: String, case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String]) url: Option[String], fileId: Option[String])
case class AccountEditForm(password: Option[String], mailAddress: String, case class AccountEditForm(password: Option[String], fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String], clearImage: Boolean) url: Option[String], fileId: Option[String], clearImage: Boolean)
val newForm = mapping( val newForm = mapping(
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))), "userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" , text(required, maxlength(20)))), "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()))), "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()))) "fileId" -> trim(label("File ID" , optional(text())))
@@ -31,6 +30,7 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
val editForm = mapping( val editForm = mapping(
"password" -> trim(label("Password" , optional(text(maxlength(20))))), "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")))), "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()))), "fileId" -> trim(label("File ID" , optional(text()))),
@@ -84,6 +84,7 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
getAccountByUserName(userName).map { account => getAccountByUserName(userName).map { account =>
updateAccount(account.copy( updateAccount(account.copy(
password = form.password.map(sha1).getOrElse(account.password), password = form.password.map(sha1).getOrElse(account.password),
fullName = form.fullName,
mailAddress = form.mailAddress, mailAddress = form.mailAddress,
url = form.url)) url = form.url))
@@ -94,6 +95,27 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
} getOrElse NotFound } 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"){ get("/register"){
if(loadSystemSettings().allowAccountRegistration){ if(loadSystemSettings().allowAccountRegistration){
if(context.loginAccount.isDefined){ if(context.loginAccount.isDefined){
@@ -106,7 +128,7 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
post("/register", newForm){ form => post("/register", newForm){ form =>
if(loadSystemSettings().allowAccountRegistration){ if(loadSystemSettings().allowAccountRegistration){
createAccount(form.userName, sha1(form.password), form.mailAddress, false, form.url) createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url)
updateImage(form.userName, form.fileId, false) updateImage(form.userName, form.fileId, false)
redirect("/signin") redirect("/signin")
} else NotFound } else NotFound

View File

@@ -1,24 +1,27 @@
package app package app
import _root_.util.Directory._ import _root_.util.Directory._
import _root_.util.{FileUtil, Validations} import _root_.util.Implicits._
import _root_.util.ControlUtil._
import _root_.util.{StringUtil, FileUtil, Validations, Keys}
import org.scalatra._ import org.scalatra._
import org.scalatra.json._ import org.scalatra.json._
import org.json4s._ import org.json4s._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import model.Account import model.Account
import scala.Some import service.{SystemSettingsService, AccountService}
import service.AccountService
import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest} import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest}
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import javax.servlet.{FilterChain, ServletResponse, ServletRequest} import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
import org.scalatra.i18n._
/** /**
* Provides generic features for controller implementations. * Provides generic features for controller implementations.
*/ */
abstract class ControllerBase extends ScalatraFilter abstract class ControllerBase extends ScalatraFilter
with ClientSideValidationFormSupport with JacksonJsonSupport with Validations { with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations
with SystemSettingsService {
implicit val jsonFormats = DefaultFormats implicit val jsonFormats = DefaultFormats
@@ -32,10 +35,10 @@ abstract class ControllerBase extends ScalatraFilter
val path = httpRequest.getRequestURI.substring(context.length) val path = httpRequest.getRequestURI.substring(context.length)
if(path.startsWith("/console/")){ if(path.startsWith("/console/")){
val account = httpRequest.getSession.getAttribute("LOGIN_ACCOUNT").asInstanceOf[Account] val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
if(account == null){ if(account == null){
// Redirect to login form // Redirect to login form
httpResponse.sendRedirect(context + "/signin?" + path) httpResponse.sendRedirect(context + "/signin?" + StringUtil.urlEncode(path))
} else if(account.isAdmin){ } else if(account.isAdmin){
// H2 Console (administrators only) // H2 Console (administrators only)
chain.doFilter(request, response) chain.doFilter(request, response)
@@ -55,91 +58,72 @@ abstract class ControllerBase extends ScalatraFilter
/** /**
* Returns the context object for the request. * Returns the context object for the request.
*/ */
implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL, request) implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, request)
private def currentURL: String = { private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount)
val queryString = request.getQueryString
request.getRequestURI + (if(queryString != null) "?" + queryString else "")
}
private def LoginAccount: Option[Account] = { def ajaxGet(path : String)(action : => Any) : Route =
session.get("LOGIN_ACCOUNT") match {
case Some(x: Account) => Some(x)
case _ => None
}
}
def ajaxGet(path : String)(action : => Any) : Route = {
super.get(path){ super.get(path){
request.setAttribute("AJAX", "true") request.setAttribute(Keys.Request.Ajax, "true")
action 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 => super.ajaxGet(path, form){ form =>
request.setAttribute("AJAX", "true") request.setAttribute(Keys.Request.Ajax, "true")
action(form) action(form)
} }
}
def ajaxPost(path : String)(action : => Any) : Route = { def ajaxPost(path : String)(action : => Any) : Route =
super.post(path){ super.post(path){
request.setAttribute("AJAX", "true") request.setAttribute(Keys.Request.Ajax, "true")
action 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 => super.ajaxPost(path, form){ form =>
request.setAttribute("AJAX", "true") request.setAttribute(Keys.Request.Ajax, "true")
action(form) action(form)
} }
}
protected def NotFound() = { protected def NotFound() =
if(request.getAttribute("AJAX") == null){ if(request.hasAttribute(Keys.Request.Ajax)){
org.scalatra.NotFound(html.error("Not Found"))
} else {
org.scalatra.NotFound() org.scalatra.NotFound()
} else {
org.scalatra.NotFound(html.error("Not Found"))
} }
}
protected def Unauthorized()(implicit context: app.Context) = { protected def Unauthorized()(implicit context: app.Context) =
if(request.getAttribute("AJAX") == null){ if(request.hasAttribute(Keys.Request.Ajax)){
org.scalatra.Unauthorized()
} else {
if(context.loginAccount.isDefined){ if(context.loginAccount.isDefined){
org.scalatra.Unauthorized(redirect("/")) org.scalatra.Unauthorized(redirect("/"))
} else { } else {
if(request.getMethod.toUpperCase == "POST"){ if(request.getMethod.toUpperCase == "POST"){
org.scalatra.Unauthorized(redirect("/signin")) org.scalatra.Unauthorized(redirect("/signin"))
} else { } else {
org.scalatra.Unauthorized(redirect("/signin?redirect=" + currentURL)) org.scalatra.Unauthorized(redirect("/signin?redirect=" + StringUtil.urlEncode(
defining(request.getQueryString){ queryString =>
request.getRequestURI.substring(request.getContextPath.length) + (if(queryString != null) "?" + queryString else "")
}
)))
} }
} }
} else {
org.scalatra.Unauthorized()
} }
}
protected def baseUrl = { override def fullUrl(path: String, params: Iterable[(String, Any)] = Iterable.empty,
val url = request.getRequestURL.toString includeContextPath: Boolean = true, includeServletPath: Boolean = true)
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length)) (implicit request: HttpServletRequest, response: HttpServletResponse) =
} if (path.startsWith("http")) path
else baseUrl + url(path, params, false, false)
} }
/** /**
* Context object for the current request. * Context object for the current request.
*/ */
case class Context(path: String, loginAccount: Option[Account], currentUrl: String, request: HttpServletRequest){ case class Context(path: String, loginAccount: Option[Account], request: HttpServletRequest){
def redirectUrl = {
if(request.getParameter("redirect") != null){
request.getParameter("redirect")
} else {
currentUrl
}
}
/** /**
* Get object from cache. * Get object from cache.
@@ -147,13 +131,14 @@ case class Context(path: String, loginAccount: Option[Account], currentUrl: Stri
* If object has not been cached with the specified key then retrieves by given action. * If object has not been cached with the specified key then retrieves by given action.
* Cached object are available during a request. * Cached object are available during a request.
*/ */
def cache[A](key: String)(action: => A): A = { def cache[A](key: String)(action: => A): A =
Option(request.getAttribute("cache." + key).asInstanceOf[A]).getOrElse { defining(Keys.Request.Cache(key)){ cacheKey =>
val newObject = action Option(request.getAttribute(cacheKey).asInstanceOf[A]).getOrElse {
request.setAttribute("cache." + key, newObject) val newObject = action
newObject request.setAttribute(cacheKey, newObject)
newObject
}
} }
}
} }
@@ -163,7 +148,7 @@ case class Context(path: String, loginAccount: Option[Account], currentUrl: Stri
trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase { trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase {
self: AccountService => self: AccountService =>
protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit = { protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit =
if(clearImage){ if(clearImage){
getAccountByUserName(userName).flatMap(_.image).map { image => getAccountByUserName(userName).flatMap(_.image).map { image =>
new java.io.File(getUserUploadDir(userName), image).delete() new java.io.File(getUserUploadDir(userName), image).delete()
@@ -179,16 +164,15 @@ trait AccountManagementControllerBase extends ControllerBase with FileUploadCont
updateAvatarImage(userName, Some(filename)) updateAvatarImage(userName, Some(filename))
} }
} }
}
protected def uniqueUserName: Constraint = new Constraint(){ protected def uniqueUserName: Constraint = new Constraint(){
override def validate(name: String, value: String): Option[String] = override def validate(name: String, value: String, messages: Messages): Option[String] =
getAccountByUserName(value).map { _ => "User already exists." } getAccountByUserName(value, true).map { _ => "User already exists." }
} }
protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){ protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, String]): Option[String] = override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
getAccountByMailAddress(value) getAccountByMailAddress(value, true)
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) } .filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
.map { _ => "Mail address is already registered." } .map { _ => "Mail address is already registered." }
} }
@@ -215,12 +199,7 @@ trait FileUploadControllerBase {
def removeTemporaryFiles()(implicit session: HttpSession): Unit = def removeTemporaryFiles()(implicit session: HttpSession): Unit =
FileUtils.deleteDirectory(TemporaryDir) FileUtils.deleteDirectory(TemporaryDir)
def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] = { def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] =
val filename = Option(session.getAttribute("upload_" + fileId).asInstanceOf[String]) session.getAndRemove[String](Keys.Session.Upload(fileId))
if(filename.isDefined){
session.removeAttribute("upload_" + fileId)
}
filename
}
} }

View File

@@ -1,14 +1,14 @@
package app package app
import util.Directory._ import util.Directory._
import util.ControlUtil._
import util._ import util._
import service._ import service._
import java.io.File
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.apache.commons.io._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.lib.PersonIdent import org.eclipse.jgit.lib.{FileMode, Constants}
import scala.Some import org.eclipse.jgit.dircache.DirCache
import org.scalatra.i18n.Messages
class CreateRepositoryController extends CreateRepositoryControllerBase class CreateRepositoryController extends CreateRepositoryControllerBase
with RepositoryService with AccountService with WikiService with LabelsService with ActivityService with RepositoryService with AccountService with WikiService with LabelsService with ActivityService
@@ -73,32 +73,26 @@ trait CreateRepositoryControllerBase extends ControllerBase {
JGitUtil.initRepository(gitdir) JGitUtil.initRepository(gitdir)
if(form.createReadme){ if(form.createReadme){
val tmpdir = getInitRepositoryDir(form.owner, form.name) using(Git.open(gitdir)){ git =>
try { val builder = DirCache.newInCore.builder()
// Clone the repository val inserter = git.getRepository.newObjectInserter()
Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call 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"
}
// Create README.md builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
FileUtils.writeStringToFile(new File(tmpdir, "README.md"), inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
if(form.description.nonEmpty){ builder.finish()
form.name + "\n" +
"===============\n" +
"\n" +
form.description.get
} else {
form.name + "\n" +
"===============\n"
}, "UTF-8")
val git = Git.open(tmpdir) JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
git.add.addFilepattern("README.md").call loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
git.commit
.setCommitter(new PersonIdent(loginUserName, loginAccount.mailAddress))
.setMessage("Initial commit").call
git.push.call
} finally {
FileUtils.deleteDirectory(tmpdir)
} }
} }
@@ -119,54 +113,62 @@ trait CreateRepositoryControllerBase extends ControllerBase {
val loginUserName = loginAccount.userName val loginUserName = loginAccount.userName
LockUtil.lock(s"${loginUserName}/${repository.name}/create"){ LockUtil.lock(s"${loginUserName}/${repository.name}/create"){
if(getRepository(loginUserName, repository.name, baseUrl).isEmpty){ if(repository.owner == loginUserName){
// Insert to the database at first // redirect to the repository
val originUserName = repository.repository.originUserName.getOrElse(repository.owner) redirect(s"/${repository.owner}/${repository.name}")
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(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( createRepository(
repositoryName = repository.name, repositoryName = repository.name,
userName = loginUserName, userName = loginUserName,
description = repository.repository.description, description = repository.repository.description,
isPrivate = repository.repository.isPrivate, isPrivate = repository.repository.isPrivate,
originRepositoryName = Some(originRepositoryName), originRepositoryName = Some(originRepositoryName),
originUserName = Some(originUserName), originUserName = Some(originUserName),
parentRepositoryName = Some(repository.name), parentRepositoryName = Some(repository.name),
parentUserName = Some(repository.owner) parentUserName = Some(repository.owner)
) )
// Insert default labels // Insert default labels
insertDefaultLabels(loginUserName, repository.name) insertDefaultLabels(loginUserName, repository.name)
// clone repository actually // clone repository actually
JGitUtil.cloneRepository( JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name), getRepositoryDir(repository.owner, repository.name),
getRepositoryDir(loginUserName, repository.name)) getRepositoryDir(loginUserName, repository.name))
// Create Wiki repository // Create Wiki repository
JGitUtil.cloneRepository( JGitUtil.cloneRepository(
getWikiRepositoryDir(repository.owner, repository.name), getWikiRepositoryDir(repository.owner, repository.name),
getWikiRepositoryDir(loginUserName, repository.name)) getWikiRepositoryDir(loginUserName, repository.name))
// insert commit id // insert commit id
JGitUtil.withGit(getRepositoryDir(loginUserName, repository.name)){ git => using(Git.open(getRepositoryDir(loginUserName, repository.name))){ git =>
JGitUtil.getRepositoryInfo(loginUserName, repository.name, baseUrl).branchList.foreach { branch => JGitUtil.getRepositoryInfo(loginUserName, repository.name, baseUrl).branchList.foreach { branch =>
JGitUtil.getCommitLog(git, branch) match { JGitUtil.getCommitLog(git, branch) match {
case Right((commits, _)) => commits.foreach { commit => case Right((commits, _)) => commits.foreach { commit =>
if(!existsCommitId(loginUserName, repository.name, commit.id)){ if(!existsCommitId(loginUserName, repository.name, commit.id)){
insertCommitId(loginUserName, repository.name, commit.id) insertCommitId(loginUserName, repository.name, commit.id)
}
} }
case Left(_) => ???
} }
case Left(_) => ???
} }
} }
}
// Record activity // Record activity
recordForkActivity(repository.owner, repository.name, loginUserName) recordForkActivity(repository.owner, repository.name, loginUserName)
// redirect to the repository
redirect(s"/${loginUserName}/${repository.name}")
}
} }
// redirect to the repository
redirect("/%s/%s".format(loginUserName, repository.name))
} }
}) })
@@ -180,7 +182,7 @@ trait CreateRepositoryControllerBase extends ControllerBase {
} }
private def existsAccount: Constraint = new Constraint(){ private def existsAccount: Constraint = new Constraint(){
override def validate(name: String, value: String): Option[String] = override def validate(name: String, value: String, messages: Messages): Option[String] =
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
} }
@@ -188,7 +190,7 @@ trait CreateRepositoryControllerBase extends ControllerBase {
* Duplicate check for the repository name. * Duplicate check for the repository name.
*/ */
private def unique: Constraint = new Constraint(){ private def unique: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, String]): Option[String] = override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
params.get("owner").flatMap { userName => params.get("owner").flatMap { userName =>
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.") getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
} }

View File

@@ -1,7 +1,8 @@
package app package app
import service._ import service._
import util.UsersAuthenticator import util.{UsersAuthenticator, Keys}
import util.Implicits._
class DashboardController extends DashboardControllerBase class DashboardController extends DashboardControllerBase
with IssuesService with PullRequestService with RepositoryService with AccountService with IssuesService with PullRequestService with RepositoryService with AccountService
@@ -42,12 +43,10 @@ trait DashboardControllerBase extends ControllerBase {
import IssuesService._ import IssuesService._
// condition // condition
val sessionKey = "dashboard/issues" val condition = session.putAndGet(Keys.Session.DashboardIssues,
val condition = if(request.getQueryString == null) if(request.hasQueryString) IssueSearchCondition(request)
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition] else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition())
else IssueSearchCondition(request) )
session.put(sessionKey, condition)
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name) val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
@@ -75,15 +74,10 @@ trait DashboardControllerBase extends ControllerBase {
import PullRequestService._ import PullRequestService._
// condition // condition
val sessionKey = "dashboard/pulls" val condition = session.putAndGet(Keys.Session.DashboardPulls, {
val condition = { if(request.hasQueryString) IssueSearchCondition(request)
if(request.getQueryString == null) else session.getAs[IssueSearchCondition](Keys.Session.DashboardPulls).getOrElse(IssueSearchCondition())
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition] }.copy(repo = repository))
else
IssueSearchCondition(request)
}.copy(repo = repository)
session.put(sessionKey, condition)
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name) val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
@@ -93,10 +87,6 @@ trait DashboardControllerBase extends ControllerBase {
val counts = countIssueGroupByRepository( val counts = countIssueGroupByRepository(
IssueSearchCondition().copy(state = condition.state), Map.empty, true, repositories: _*) IssueSearchCondition().copy(state = condition.state), Map.empty, true, repositories: _*)
getRepositoryNamesOfUser(userName).map { repoName =>
(userName, repoName, counts.collectFirst { case (_, repoName, count) => count })
}
dashboard.html.pulls( dashboard.html.pulls(
pulls.html.listparts( pulls.html.listparts(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, repositories: _*), searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, repositories: _*),

View File

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

View File

@@ -5,12 +5,17 @@ import service._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
class IndexController extends IndexControllerBase class IndexController extends IndexControllerBase
with RepositoryService with SystemSettingsService with ActivityService with AccountService with RepositoryService with ActivityService with AccountService with UsersAuthenticator
with UsersAuthenticator
trait IndexControllerBase extends ControllerBase { trait IndexControllerBase extends ControllerBase {
self: RepositoryService with SystemSettingsService with ActivityService with AccountService self: RepositoryService with ActivityService with AccountService with UsersAuthenticator =>
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)
get("/"){ get("/"){
val loginAccount = context.loginAccount val loginAccount = context.loginAccount
@@ -22,6 +27,44 @@ trait IndexControllerBase extends ControllerBase {
) )
} }
get("/signin"){
val redirect = params.get("redirect")
if(redirect.isDefined && redirect.get.startsWith("/")){
flash += Keys.Flash.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)
flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl =>
if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){
redirect("/")
} else {
redirect(redirectUrl)
}
}.getOrElse {
redirect("/")
}
}
/** /**
* JSON API for collaborator completion. * JSON API for collaborator completion.
* *
@@ -30,7 +73,7 @@ trait IndexControllerBase extends ControllerBase {
get("/_user/proposals")(usersOnly { get("/_user/proposals")(usersOnly {
contentType = formats("json") contentType = formats("json")
org.json4s.jackson.Serialization.write( org.json4s.jackson.Serialization.write(
Map("options" -> getAllUsers.filter(!_.isGroupAccount).map(_.userName).toArray) Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray)
) )
}) })

View File

@@ -4,15 +4,18 @@ import jp.sf.amateras.scalatra.forms._
import service._ import service._
import IssuesService._ import IssuesService._
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator, Notifier} import util._
import util.Implicits._
import util.ControlUtil._
import org.scalatra.Ok import org.scalatra.Ok
import model.Issue
class IssuesController extends IssuesControllerBase class IssuesController extends IssuesControllerBase
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
trait IssuesControllerBase extends ControllerBase { 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 => with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
case class IssueCreateForm(title: String, content: Option[String], case class IssueCreateForm(title: String, content: Option[String],
@@ -57,79 +60,84 @@ trait IssuesControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/issues/:id")(referrersOnly { repository => get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
val owner = repository.owner defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) =>
val name = repository.name getIssue(owner, name, issueId) map {
val issueId = params("id") issues.html.issue(
getIssue(owner, name, issueId) map {
issues.html.issue(
_, _,
getComments(owner, name, issueId.toInt), getComments(owner, name, issueId.toInt),
getIssueLabels(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), getMilestonesWithIssueCount(owner, name),
getLabels(owner, name), getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount), hasWritePermission(owner, name, context.loginAccount),
repository) repository)
} getOrElse NotFound } getOrElse NotFound
}
}) })
get("/:owner/:repository/issues/new")(readableUsersOnly { repository => get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
val owner = repository.owner defining(repository.owner, repository.name){ case (owner, name) =>
val name = repository.name issues.html.create(
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
issues.html.create( getMilestones(owner, name),
(getCollaborators(owner, name) :+ owner).sorted, getLabels(owner, name),
getMilestones(owner, name), hasWritePermission(owner, name, context.loginAccount),
getLabels(owner, name), repository)
hasWritePermission(owner, name, context.loginAccount), }
repository)
}) })
post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
val owner = repository.owner defining(repository.owner, repository.name){ case (owner, name) =>
val name = repository.name val writable = hasWritePermission(owner, name, context.loginAccount)
val writable = hasWritePermission(owner, name, context.loginAccount) val userName = context.loginAccount.get.userName
val userName = context.loginAccount.get.userName
// insert issue // insert issue
val issueId = createIssue(owner, name, userName, form.title, form.content, val issueId = createIssue(owner, name, userName, form.title, form.content,
if(writable) form.assignedUserName else None, if(writable) form.assignedUserName else None,
if(writable) form.milestoneId else None) if(writable) form.milestoneId else None)
// insert labels // insert labels
if(writable){ if(writable){
form.labelNames.map { value => form.labelNames.map { value =>
val labels = getLabels(owner, name) val labels = getLabels(owner, name)
value.split(",").foreach { labelName => value.split(",").foreach { labelName =>
labels.find(_.labelName == labelName).map { label => labels.find(_.labelName == labelName).map { label =>
registerIssueLabel(owner, name, issueId, label.labelId) registerIssueLabel(owner, name, issueId, label.labelId)
}
} }
} }
} }
// record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
// extract references and create refer comment
getIssue(owner, name, issueId.toString).foreach { issue =>
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
}
// 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)
// notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgIssue(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
}
redirect(s"/${owner}/${name}/issues/${issueId}")
}) })
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) => ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
val owner = repository.owner defining(repository.owner, repository.name){ case (owner, name) =>
val name = repository.name getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){
// update issue
updateIssue(owner, name, issue.issueId, form.title, form.content)
// extract references and create refer comment
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
getIssue(owner, name, params("id")).map { issue => redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
if(isEditable(owner, name, issue.openedUserName)){ } else Unauthorized
updateIssue(owner, name, issue.issueId, form.title, form.content) } getOrElse NotFound
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") }
} else Unauthorized
} getOrElse NotFound
}) })
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
@@ -147,15 +155,24 @@ trait IssuesControllerBase extends ControllerBase {
}) })
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
val owner = repository.owner defining(repository.owner, repository.name){ case (owner, name) =>
val name = repository.name getComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){
updateComment(comment.commentId, form.content)
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
} else Unauthorized
} getOrElse NotFound
}
})
getComment(owner, name, params("id")).map { comment => ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository =>
if(isEditable(owner, name, comment.commentedUserName)){ defining(repository.owner, repository.name){ case (owner, name) =>
updateComment(comment.commentId, form.content) getComment(owner, name, params("id")).map { comment =>
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}") if(isEditable(owner, name, comment.commentedUserName)){
} else Unauthorized Ok(deleteComment(comment.commentId))
} getOrElse NotFound } else Unauthorized
} getOrElse NotFound
}
}) })
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository => ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
@@ -194,17 +211,17 @@ trait IssuesControllerBase extends ControllerBase {
}) })
ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
val issueId = params("id").toInt defining(params("id").toInt){ issueId =>
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) }
}) })
ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository =>
val issueId = params("id").toInt defining(params("id").toInt){ issueId =>
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) }
}) })
ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
@@ -223,41 +240,41 @@ trait IssuesControllerBase extends ControllerBase {
}) })
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
val action = params.get("value") defining(params.get("value")){ action =>
executeBatch(repository) {
executeBatch(repository) { handleComment(_, None, repository)( _ => action)
handleComment(_, None, repository)( _ => action)
}
})
post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
val labelId = params("value").toInt
executeBatch(repository) { issueId =>
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
registerIssueLabel(repository.owner, repository.name, issueId, labelId)
} }
} }
}) })
post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository => post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
val value = assignedUserName("value") 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
})
executeBatch(repository) { post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
updateAssignedUserName(repository.owner, repository.name, _, value) defining(assignedUserName("value")){ value =>
executeBatch(repository) {
updateAssignedUserName(repository.owner, repository.name, _, value)
}
} }
}) })
post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository => post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository =>
val value = milestoneId("value") defining(milestoneId("value")){ value =>
executeBatch(repository) {
executeBatch(repository) { updateMilestoneId(repository.owner, repository.name, _, value)
updateMilestoneId(repository.owner, repository.name, _, value) }
} }
}) })
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "") val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
val milestoneId = (key: String) => params.get(key) collect { case x if x.trim != "" => x.toInt } 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 = private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
@@ -267,95 +284,109 @@ trait IssuesControllerBase extends ControllerBase {
redirect(s"/${repository.owner}/${repository.name}/issues") redirect(s"/${repository.owner}/${repository.name}/issues")
} }
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
StringUtil.extractIssueId(message).foreach { issueId =>
if(getIssue(owner, repository, issueId).isDefined){
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt,
fromIssue.issueId + ":" + fromIssue.title, "refer")
}
}
}
/** /**
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]] * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
*/ */
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo) private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
(getAction: model.Issue => Option[String] = (getAction: model.Issue => Option[String] =
p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = { p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
val owner = repository.owner
val name = repository.name
val userName = context.loginAccount.get.userName
getIssue(owner, name, issueId.toString) map { issue => defining(repository.owner, repository.name){ case (owner, name) =>
val (action, recordActivity) = val userName = context.loginAccount.get.userName
getAction(issue)
.collect { getIssue(owner, name, issueId.toString) map { issue =>
val (action, recordActivity) =
getAction(issue)
.collect {
case "close" => true -> (Some("close") -> case "close" => true -> (Some("close") ->
Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _)) Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
case "reopen" => false -> (Some("reopen") -> case "reopen" => false -> (Some("reopen") ->
Some(recordReopenIssueActivity _)) Some(recordReopenIssueActivity _))
} }
.map { case (closed, t) => .map { case (closed, t) =>
updateClosed(owner, name, issueId, closed) updateClosed(owner, name, issueId, closed)
t t
} }
.getOrElse(None -> None) .getOrElse(None -> None)
val commentId = content val commentId = content
.map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") ) .map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") )
.getOrElse ( action.get.capitalize -> action.get ) .getOrElse ( action.get.capitalize -> action.get )
match { match {
case (content, action) => createComment(owner, name, userName, issueId, content, action) case (content, action) => createComment(owner, name, userName, issueId, content, action)
} }
// record activity // record activity
content foreach { content foreach {
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _) (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
(owner, name, userName, issueId, _) (owner, name, userName, issueId, _)
} }
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) ) recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
// notifications // extract references and create refer comment
Notifier() match { content.map { content =>
case f => createReferComment(owner, name, issue, content)
content foreach { }
f.toNotify(repository, issueId, _){
Notifier.msgComment(s"${baseUrl}/${owner}/${name}/${ // notifications
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}") 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 {
action foreach { f.toNotify(repository, issueId, _){
f.toNotify(repository, issueId, _){ Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/issues/${issueId}") }
} }
} }
}
issue -> commentId issue -> commentId
}
} }
} }
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = { private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
val owner = repository.owner defining(repository.owner, repository.name){ case (owner, repoName) =>
val repoName = repository.name val filterUser = Map(filter -> params.getOrElse("userName", ""))
val filterUser = Map(filter -> params.getOrElse("userName", "")) val page = IssueSearchCondition.page(request)
val page = IssueSearchCondition.page(request) val sessionKey = Keys.Session.Issues(owner, repoName)
val sessionKey = s"${owner}/${repoName}/issues"
// retrieve search condition // retrieve search condition
val condition = if(request.getQueryString == null){ val condition = session.putAndGet(sessionKey,
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition] if(request.hasQueryString) IssueSearchCondition(request)
} else IssueSearchCondition(request) else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
session.put(sessionKey, condition) issues.html.list(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
issues.html.list( page,
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), (getCollaborators(owner, repoName) :+ owner).sorted,
page, getMilestones(owner, repoName),
(getCollaborators(owner, repoName) :+ owner).sorted, getLabels(owner, repoName),
getMilestones(owner, repoName), countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName),
getLabels(owner, repoName), countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName),
countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName), countIssue(condition, Map.empty, false, owner -> repoName),
countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName), context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)),
countIssue(condition, Map.empty, false, owner -> repoName), context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)),
context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)), countIssueGroupByLabels(owner, repoName, condition, filterUser),
context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)), condition,
countIssueGroupByLabels(owner, repoName, condition, filterUser), filter,
condition, repository,
filter, hasWritePermission(owner, repoName, context.loginAccount))
repository, }
hasWritePermission(owner, repoName, context.loginAccount))
} }
} }

View File

@@ -3,6 +3,7 @@ package app
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import service._ import service._
import util.CollaboratorsAuthenticator import util.CollaboratorsAuthenticator
import org.scalatra.i18n.Messages
class LabelsController extends LabelsControllerBase class LabelsController extends LabelsControllerBase
with LabelsService with RepositoryService with AccountService with CollaboratorsAuthenticator with LabelsService with RepositoryService with AccountService with CollaboratorsAuthenticator
@@ -51,7 +52,7 @@ trait LabelsControllerBase extends ControllerBase {
* Constraint for the identifier such as user name, repository name or page name. * Constraint for the identifier such as user name, repository name or page name.
*/ */
private def labelName: Constraint = new Constraint(){ private def labelName: Constraint = new Constraint(){
override def validate(name: String, value: String): Option[String] = override def validate(name: String, value: String, messages: Messages): Option[String] =
if(!value.matches("^[^,]+$")){ if(!value.matches("^[^,]+$")){
Some(s"${name} contains invalid character.") Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){ } else if(value.startsWith("_") || value.startsWith("-")){

View File

@@ -4,6 +4,7 @@ import jp.sf.amateras.scalatra.forms._
import service._ import service._
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator} import util.{CollaboratorsAuthenticator, ReferrerAuthenticator}
import util.Implicits._
class MilestonesController extends MilestonesControllerBase class MilestonesController extends MilestonesControllerBase
with MilestonesService with RepositoryService with AccountService with MilestonesService with RepositoryService with AccountService
@@ -39,34 +40,44 @@ trait MilestonesControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/issues/milestones/:milestoneId/edit")(collaboratorsOnly { repository => 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) => post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(collaboratorsOnly { (form, repository) =>
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone => params("milestoneId").toIntOpt.flatMap{ milestoneId =>
updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate)) getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
redirect(s"/${repository.owner}/${repository.name}/issues/milestones") updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate))
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
}
} getOrElse NotFound } getOrElse NotFound
}) })
get("/:owner/:repository/issues/milestones/:milestoneId/close")(collaboratorsOnly { repository => get("/:owner/:repository/issues/milestones/:milestoneId/close")(collaboratorsOnly { repository =>
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone => params("milestoneId").toIntOpt.flatMap{ milestoneId =>
closeMilestone(milestone) getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
redirect(s"/${repository.owner}/${repository.name}/issues/milestones") closeMilestone(milestone)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
}
} getOrElse NotFound } getOrElse NotFound
}) })
get("/:owner/:repository/issues/milestones/:milestoneId/open")(collaboratorsOnly { repository => get("/:owner/:repository/issues/milestones/:milestoneId/open")(collaboratorsOnly { repository =>
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone => params("milestoneId").toIntOpt.flatMap{ milestoneId =>
openMilestone(milestone) getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
redirect(s"/${repository.owner}/${repository.name}/issues/milestones") openMilestone(milestone)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
}
} getOrElse NotFound } getOrElse NotFound
}) })
get("/:owner/:repository/issues/milestones/:milestoneId/delete")(collaboratorsOnly { repository => get("/:owner/:repository/issues/milestones/:milestoneId/delete")(collaboratorsOnly { repository =>
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone => params("milestoneId").toIntOpt.flatMap{ milestoneId =>
deleteMilestone(repository.owner, repository.name, milestone.milestoneId) getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
redirect(s"/${repository.owner}/${repository.name}/issues/milestones") deleteMilestone(repository.owner, repository.name, milestone.milestoneId)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
}
} getOrElse NotFound } getOrElse NotFound
}) })

View File

@@ -1,40 +1,45 @@
package app package app
import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier} import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier, Keys}
import util.Directory._ import util.Directory._
import util.Implicits._ import util.Implicits._
import util.ControlUtil._
import service._ import service._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.transport.RefSpec import org.eclipse.jgit.transport.RefSpec
import org.apache.commons.io.FileUtils
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
import org.eclipse.jgit.lib.PersonIdent import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
import org.eclipse.jgit.api.MergeCommand.FastForwardMode
import service.IssuesService._ import service.IssuesService._
import service.PullRequestService._ import service.PullRequestService._
import util.JGitUtil.DiffInfo import util.JGitUtil.DiffInfo
import scala.Some
import service.RepositoryService.RepositoryTreeNode import service.RepositoryService.RepositoryTreeNode
import util.JGitUtil.CommitInfo 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 class PullRequestsController extends PullRequestsControllerBase
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with ActivityService with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
with ReferrerAuthenticator with CollaboratorsAuthenticator with ActivityService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator
trait PullRequestsControllerBase extends ControllerBase { trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService with IssuesService with MilestonesService with ActivityService with PullRequestService self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
with ReferrerAuthenticator with CollaboratorsAuthenticator => with ActivityService with PullRequestService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
val pullRequestForm = mapping( val pullRequestForm = mapping(
"title" -> trim(label("Title" , text(required, maxlength(100)))), "title" -> trim(label("Title" , text(required, maxlength(100)))),
"content" -> trim(label("Content", optional(text()))), "content" -> trim(label("Content", optional(text()))),
"targetUserName" -> trim(text(required, maxlength(100))), "targetUserName" -> trim(text(required, maxlength(100))),
"targetBranch" -> trim(text(required, maxlength(100))), "targetBranch" -> trim(text(required, maxlength(100))),
"requestUserName" -> trim(text(required, maxlength(100))), "requestUserName" -> trim(text(required, maxlength(100))),
"requestBranch" -> trim(text(required, maxlength(100))), "requestRepositoryName" -> trim(text(required, maxlength(100))),
"commitIdFrom" -> trim(text(required, maxlength(40))), "requestBranch" -> trim(text(required, maxlength(100))),
"commitIdTo" -> trim(text(required, maxlength(40))) "commitIdFrom" -> trim(text(required, maxlength(40))),
"commitIdTo" -> trim(text(required, maxlength(40)))
)(PullRequestForm.apply) )(PullRequestForm.apply)
val mergeForm = mapping( val mergeForm = mapping(
@@ -47,6 +52,7 @@ trait PullRequestsControllerBase extends ControllerBase {
targetUserName: String, targetUserName: String,
targetBranch: String, targetBranch: String,
requestUserName: String, requestUserName: String,
requestRepositoryName: String,
requestBranch: String, requestBranch: String,
commitIdFrom: String, commitIdFrom: String,
commitIdTo: String) commitIdTo: String)
@@ -62,156 +68,150 @@ trait PullRequestsControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/pull/:id")(referrersOnly { repository => get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
val owner = repository.owner params("id").toIntOpt.flatMap{ issueId =>
val name = repository.name val owner = repository.owner
val issueId = params("id").toInt 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)
getPullRequest(owner, name, issueId) map { case(issue, pullreq) => pulls.html.pullreq(
JGitUtil.withGit(getRepositoryDir(owner, name)){ git => issue, pullreq,
val requestCommitId = git.getRepository.resolve(pullreq.requestBranch) getComments(owner, name, issueId),
getIssueLabels(owner, name, issueId.toInt),
val (commits, diffs) = (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo) getMilestonesWithIssueCount(owner, name),
getLabels(owner, name),
pulls.html.pullreq( commits,
issue, pullreq, diffs,
getComments(owner, name, issueId.toInt), hasWritePermission(owner, name, context.loginAccount),
(getCollaborators(owner, name) :+ owner).sorted, repository)
getMilestonesWithIssueCount(owner, name), }
commits,
diffs,
if(issue.closed){
false
} else {
checkConflict(owner, name, pullreq.branch, owner, name, pullreq.requestBranch)
},
hasWritePermission(owner, name, context.loginAccount),
repository,
s"${baseUrl}${context.path}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
} }
} 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
})
get("/:owner/:repository/pull/:id/delete/:branchName")(collaboratorsOnly { repository =>
params("id").toIntOpt.map { issueId =>
val branchName = params("branchName")
val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.branchDelete().setBranchNames(branchName).call()
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
}
}
createComment(repository.owner, repository.name, userName, issueId, branchName, "delete_branch")
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} getOrElse NotFound } getOrElse NotFound
}) })
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) => post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
LockUtil.lock(s"${repository.owner}/${repository.name}/merge"){ params("id").toIntOpt.flatMap { issueId =>
val issueId = params("id").toInt 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)
getPullRequest(repository.owner, repository.name, issueId).map { case (issue, pullreq) => // record activity
val remote = getRepositoryDir(repository.owner, repository.name) recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
val tmpdir = new java.io.File(getTemporaryDir(repository.owner, repository.name), s"merge-${issueId}")
val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(pullreq.branch).call
try { // merge
// mark issue as merged and close. val mergeBaseRefName = s"refs/heads/${pullreq.branch}"
val loginAccount = context.loginAccount.get val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
createComment(repository.owner, repository.name, loginAccount.userName, issueId, form.message, "merge") val mergeBaseTip = git.getRepository.resolve(mergeBaseRefName)
createComment(repository.owner, repository.name, loginAccount.userName, issueId, "Close", "close") val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
updateClosed(repository.owner, repository.name, issueId, true) val conflicted = try {
!merger.merge(mergeBaseTip, mergeTip)
// record activity } catch {
recordMergeActivity(repository.owner, repository.name, loginAccount.userName, issueId, form.message) case e: NoMergeBaseException => true
// TODO apply ref comment
// fetch pull request to temporary working repository
val pullRequestBranchName = s"gitbucket-pullrequest-${issueId}"
git.fetch
.setRemote(getRepositoryDir(repository.owner, repository.name).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/pull/${issueId}/head:refs/heads/${pullRequestBranchName}")).call
// merge pull request
git.checkout.setName(pullreq.branch).call
val result = git.merge
.include(git.getRepository.resolve(pullRequestBranchName))
.setFastForward(FastForwardMode.NO_FF)
.setCommit(false)
.call
if(result.getConflicts != null){
throw new RuntimeException("This pull request can't merge automatically.")
}
// merge commit
git.getRepository.writeMergeCommitMsg(
s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestRepositoryName}\n"
+ form.message)
git.commit
.setCommitter(new PersonIdent(loginAccount.userName, loginAccount.mailAddress))
.call
// push
git.push.call
val (commits, _) = getRequestCompareInfo(repository.owner, repository.name, pullreq.commitIdFrom,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
commits.flatten.foreach { commit =>
if(!existsCommitId(repository.owner, repository.name, commit.id)){
insertCommitId(repository.owner, repository.name, commit.id)
} }
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}")
} }
// notifications
Notifier().toNotify(repository, issueId, "merge"){
Notifier.msgStatus(s"${baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
}
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} finally {
git.getRepository.close
FileUtils.deleteDirectory(tmpdir)
} }
} getOrElse NotFound }
} } getOrElse NotFound
}) })
/**
* Checks whether conflict will be caused in merging.
* Returns true if conflict will be caused.
*/
private def checkConflict(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
// TODO Are there more quick way?
LockUtil.lock(s"${userName}/${repositoryName}/merge-check"){
val remote = getRepositoryDir(userName, repositoryName)
val tmpdir = new java.io.File(getTemporaryDir(userName, repositoryName), "merge-check")
if(tmpdir.exists()){
FileUtils.deleteDirectory(tmpdir)
}
val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(branch).call
try {
git.checkout.setName(branch).call
git.fetch
.setRemote(getRepositoryDir(requestUserName, requestRepositoryName).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/heads/${requestBranch}")).call
val result = git.merge
.include(git.getRepository.resolve("FETCH_HEAD"))
.setCommit(false).call
result.getConflicts != null
} finally {
git.getRepository.close
FileUtils.deleteDirectory(tmpdir)
}
}
}
get("/:owner/:repository/compare")(referrersOnly { forkedRepository => get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match { (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(originUserName), Some(originRepositoryName)) => { case (Some(originUserName), Some(originRepositoryName)) => {
getRepository(originUserName, originRepositoryName, baseUrl).map { originRepository => getRepository(originUserName, originRepositoryName, baseUrl).map { originRepository =>
withGit( using(
getRepositoryDir(originUserName, originRepositoryName), Git.open(getRepositoryDir(originUserName, originRepositoryName)),
getRepositoryDir(forkedRepository.owner, forkedRepository.name) Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
){ (oldGit, newGit) => ){ (oldGit, newGit) =>
val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2 val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2
val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2 val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2
@@ -221,59 +221,96 @@ trait PullRequestsControllerBase extends ControllerBase {
} getOrElse NotFound } getOrElse NotFound
} }
case _ => { case _ => {
JGitUtil.withGit(getRepositoryDir(forkedRepository.owner, forkedRepository.name)){ git => using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
val defaultBranch = JGitUtil.getDefaultBranch(git, forkedRepository).get._2 JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) =>
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${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 { repository => get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat") val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner) val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner) val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
(getRepository(originOwner, repository.name, baseUrl), (for(
getRepository(forkedOwner, repository.name, baseUrl)) match { originRepositoryName <- if(originOwner == forkedOwner){
case (Some(originRepository), Some(forkedRepository)) => { Some(forkedRepository.name)
withGit( } else {
getRepositoryDir(originOwner, repository.name), forkedRepository.repository.originRepositoryName.orElse {
getRepositoryDir(forkedOwner, repository.name) getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
){ case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
val forkedId = getForkedCommitId(oldGit, newGit,
originOwner, repository.name, originBranch,
forkedOwner, repository.name, forkedBranch)
val oldId = oldGit.getRepository.resolve(forkedId)
val newId = newGit.getRepository.resolve(forkedBranch)
val (commits, diffs) = getRequestCompareInfo(
originOwner, repository.name, oldId.getName,
forkedOwner, repository.name, newId.getName)
pulls.html.compare(
commits,
diffs,
repository.repository.originUserName.map { userName =>
userName :: getForkedRepositories(userName, repository.name)
} getOrElse List(repository.owner),
originBranch,
forkedBranch,
oldId.getName,
newId.getName,
checkConflict(originOwner, repository.name, originBranch, forkedOwner, repository.name, forkedBranch),
repository,
originRepository,
forkedRepository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
} }
};
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))
} }
case _ => NotFound }) 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) => post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
@@ -295,15 +332,15 @@ trait PullRequestsControllerBase extends ControllerBase {
issueId = issueId, issueId = issueId,
originBranch = form.targetBranch, originBranch = form.targetBranch,
requestUserName = form.requestUserName, requestUserName = form.requestUserName,
requestRepositoryName = repository.name, requestRepositoryName = form.requestRepositoryName,
requestBranch = form.requestBranch, requestBranch = form.requestBranch,
commitIdFrom = form.commitIdFrom, commitIdFrom = form.commitIdFrom,
commitIdTo = form.commitIdTo) commitIdTo = form.commitIdTo)
// fetch requested branch // fetch requested branch
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.fetch git.fetch
.setRemote(getRepositoryDir(form.requestUserName, repository.name).toURI.toString) .setRemote(getRepositoryDir(form.requestUserName, form.requestRepositoryName).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head")) .setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head"))
.call .call
} }
@@ -320,16 +357,58 @@ trait PullRequestsControllerBase extends ControllerBase {
}) })
/** /**
* Handles w Git object simultaneously. * Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
*/ */
private def withGit[T](oldDir: java.io.File, newDir: java.io.File)(action: (Git, Git) => T): T = { private def checkConflict(userName: String, repositoryName: String, branch: String,
val oldGit = Git.open(oldDir) requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
val newGit = Git.open(newDir) LockUtil.lock(s"${userName}/${repositoryName}/merge-check"){
try { using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
action(oldGit, newGit) val remoteRefName = s"refs/heads/${branch}"
} finally { val tmpRefName = s"refs/merge-check/${userName}/${branch}"
oldGit.getRepository.close val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true)
newGit.getRepository.close 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
}
}
} }
} }
@@ -359,23 +438,22 @@ trait PullRequestsControllerBase extends ControllerBase {
private def getForkedCommitId(oldGit: Git, newGit: Git, userName: String, repositoryName: String, branch: String, private def getForkedCommitId(oldGit: Git, newGit: Git, userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): String = requestUserName: String, requestRepositoryName: String, requestBranch: String): String =
JGitUtil.getCommitLogs(newGit, requestBranch, true){ commit => JGitUtil.getCommitLogs(newGit, requestBranch, true){ commit =>
existsCommitId(userName, repositoryName, commit.getName) && existsCommitId(userName, repositoryName, commit.getName) && JGitUtil.getBranchesOfCommit(oldGit, commit.getName).contains(branch)
JGitUtil.getBranchesOfCommit(oldGit, commit.getName).contains(branch)
}.head.id }.head.id
private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String, private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = { requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = {
withGit( using(
getRepositoryDir(userName, repositoryName), Git.open(getRepositoryDir(userName, repositoryName)),
getRepositoryDir(requestUserName, requestRepositoryName) Git.open(getRepositoryDir(requestUserName, requestRepositoryName))
){ (oldGit, newGit) => ){ (oldGit, newGit) =>
val oldId = oldGit.getRepository.resolve(branch) val oldId = oldGit.getRepository.resolve(branch)
val newId = newGit.getRepository.resolve(requestCommitId) val newId = newGit.getRepository.resolve(requestCommitId)
val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit => val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
new CommitInfo(revCommit) new CommitInfo(revCommit)
}.toList.splitWith{ (commit1, commit2) => }.toList.splitWith { (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time) view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
} }
@@ -385,31 +463,29 @@ trait PullRequestsControllerBase extends ControllerBase {
} }
} }
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) = { private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
val owner = repository.owner defining(repository.owner, repository.name){ case (owner, repoName) =>
val repoName = repository.name val filterUser = userName.map { x => Map("created_by" -> x) } getOrElse Map("all" -> "")
val filterUser = userName.map { x => Map("created_by" -> x) } getOrElse Map("all" -> "") val page = IssueSearchCondition.page(request)
val page = IssueSearchCondition.page(request) val sessionKey = Keys.Session.Pulls(owner, repoName)
val sessionKey = s"${owner}/${repoName}/pulls"
// retrieve search condition // retrieve search condition
val condition = if(request.getQueryString == null){ val condition = session.putAndGet(sessionKey,
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition] if(request.hasQueryString) IssueSearchCondition(request)
} else IssueSearchCondition(request) else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
session.put(sessionKey, condition) pulls.html.list(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
pulls.html.list( getPullRequestCountGroupByUser(condition.state == "closed", owner, Some(repoName)),
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), userName,
getPullRequestCountGroupByUser(condition.state == "closed", owner, Some(repoName)), page,
userName, countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName),
page, countIssue(condition.copy(state = "closed"), filterUser, true, owner -> repoName),
countIssue(condition.copy(state = "open"), filterUser, true, owner -> repoName), countIssue(condition, Map.empty, true, owner -> repoName),
countIssue(condition.copy(state = "closed"), filterUser, true, owner -> repoName), condition,
countIssue(condition, Map.empty, true, owner -> repoName), repository,
condition, hasWritePermission(owner, repoName, context.loginAccount))
repository, }
hasWritePermission(owner, repoName, context.loginAccount))
}
} }

View File

@@ -5,28 +5,51 @@ import util.Directory._
import util.{UsersAuthenticator, OwnerAuthenticator} import util.{UsersAuthenticator, OwnerAuthenticator}
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils 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 class RepositorySettingsController extends RepositorySettingsControllerBase
with RepositoryService with AccountService with OwnerAuthenticator with UsersAuthenticator with RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator
trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSupport { trait RepositorySettingsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with OwnerAuthenticator with UsersAuthenticator => self: RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator =>
case class OptionsForm(description: Option[String], defaultBranch: String, isPrivate: Boolean) // for repository options
case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean)
val optionsForm = mapping( val optionsForm = mapping(
"description" -> trim(label("Description" , optional(text()))), "repositoryName" -> trim(label("Description" , text(required, maxlength(40), identifier, renameRepositoryName))),
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))), "description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())) "defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
"isPrivate" -> trim(label("Repository Type", boolean()))
)(OptionsForm.apply) )(OptionsForm.apply)
// for collaborator addition
case class CollaboratorForm(userName: String) case class CollaboratorForm(userName: String)
val collaboratorForm = mapping( val collaboratorForm = mapping(
"userName" -> trim(label("Username", text(required, collaborator))) "userName" -> trim(label("Username", text(required, collaborator)))
)(CollaboratorForm.apply) )(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. * Redirect to the Options page.
*/ */
@@ -54,8 +77,21 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
repository.repository.isPrivate repository.repository.isPrivate
} getOrElse form.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." flash += "info" -> "Repository settings has been updated."
redirect(s"/${repository.owner}/${repository.name}/settings/options") redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
}) })
/** /**
@@ -89,10 +125,78 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
}) })
/** /**
* Display the delete repository page. * Display the web hook page.
*/ */
get("/:owner/:repository/settings/delete")(ownerOnly { get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
settings.html.delete(_) 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}")
}) })
/** /**
@@ -108,19 +212,55 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
redirect(s"/${repository.owner}") 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. * Provides Constraint to validate the collaborator name.
*/ */
private def collaborator: Constraint = new Constraint(){ private def collaborator: Constraint = new Constraint(){
override def validate(name: String, value: String): Option[String] = { override def validate(name: String, value: String, messages: Messages): Option[String] =
val paths = request.getRequestURI.split("/")
getAccountByUserName(value) match { getAccountByUserName(value) match {
case None => Some("User does not exist.") case None => Some("User does not exist.")
case Some(x) if(x.userName == paths(1) || getCollaborators(paths(1), paths(2)).contains(x.userName)) case Some(x) if(x.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.") => Some("User can access this repository already.")
case _ => None 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.Directory._
import util.Implicits._ import util.Implicits._
import _root_.util.{ReferrerAuthenticator, JGitUtil, FileUtil, StringUtil} import util.ControlUtil._
import _root_.util._
import service._ import service._
import org.scalatra._ import org.scalatra._
import java.io.File import java.io.File
@@ -10,15 +11,17 @@ import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib._ import org.eclipse.jgit.lib._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.eclipse.jgit.treewalk._ import org.eclipse.jgit.treewalk._
import java.util.zip.{ZipEntry, ZipOutputStream}
import scala.Some
class RepositoryViewerController extends RepositoryViewerControllerBase class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ReferrerAuthenticator with RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator
/** /**
* The repository viewer. * The repository viewer.
*/ */
trait RepositoryViewerControllerBase extends ControllerBase { trait RepositoryViewerControllerBase extends ControllerBase {
self: RepositoryService with AccountService with ReferrerAuthenticator => self: RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
/** /**
* Returns converted HTML from Markdown for preview. * Returns converted HTML from Markdown for preview.
@@ -36,7 +39,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
get("/:owner/:repository")(referrersOnly { get("/:owner/:repository")(referrersOnly {
fileList(_) fileList(_)
}) })
/** /**
* Displays the file list of the specified path and branch. * Displays the file list of the specified path and branch.
*/ */
@@ -48,15 +51,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
fileList(repository, id, path) fileList(repository, id, path)
} }
}) })
/** /**
* Displays the commit list of the specified resource. * Displays the commit list of the specified resource.
*/ */
get("/:owner/:repository/commits/*")(referrersOnly { repository => get("/:owner/:repository/commits/*")(referrersOnly { repository =>
val (branchName, path) = splitPath(repository, multiParams("splat").head) val (branchName, path) = splitPath(repository, multiParams("splat").head)
val page = params.getOrElse("page", "1").toInt val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1)
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, branchName, page, 30, path) match { JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
case Right((logs, hasNext)) => case Right((logs, hasNext)) =>
repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository, repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
@@ -75,7 +78,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val (id, path) = splitPath(repository, multiParams("splat").head) val (id, path) = splitPath(repository, multiParams("splat").head)
val raw = params.get("raw").getOrElse("false").toBoolean 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)) val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
@scala.annotation.tailrec @scala.annotation.tailrec
@@ -84,19 +87,18 @@ trait RepositoryViewerControllerBase extends ControllerBase {
case true => getPathObjectId(path, walk) case true => getPathObjectId(path, walk)
} }
val treeWalk = new TreeWalk(git.getRepository) val objectId = using(new TreeWalk(git.getRepository)){ treeWalk =>
val objectId = try {
treeWalk.addTree(revCommit.getTree) treeWalk.addTree(revCommit.getTree)
treeWalk.setRecursive(true) treeWalk.setRecursive(true)
getPathObjectId(path, treeWalk) getPathObjectId(path, treeWalk)
} finally {
treeWalk.release
} }
if(raw){ if(raw){
// Download // Download
contentType = "application/octet-stream" defining(JGitUtil.getContent(git, objectId, false).get){ bytes =>
JGitUtil.getContent(git, objectId, false).get contentType = FileUtil.getContentType(path, bytes)
bytes
}
} else { } else {
// Viewer // Viewer
val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize) val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize)
@@ -120,39 +122,54 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} }
} }
}) })
/** /**
* Displays details of the specified commit. * Displays details of the specified commit.
*/ */
get("/:owner/:repository/commit/:id")(referrersOnly { repository => get("/:owner/:repository/commit/:id")(referrersOnly { repository =>
val id = params("id") val id = params("id")
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)) defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))){ revCommit =>
JGitUtil.getDiffs(git, id) match { case (diffs, oldCommitId) =>
JGitUtil.getDiffs(git, id) match { case (diffs, oldCommitId) => repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
repo.html.commit(id, new JGitUtil.CommitInfo(revCommit), JGitUtil.getBranchesOfCommit(git, revCommit.getName),
JGitUtil.getBranchesOfCommit(git, revCommit.getName), JGitUtil.getTagsOfCommit(git, revCommit.getName),
JGitUtil.getTagsOfCommit(git, revCommit.getName), repository, diffs, oldCommitId)
repository, diffs, oldCommitId) }
} }
} }
}) })
/** /**
* Displays branches. * Displays branches.
*/ */
get("/:owner/:repository/branches")(referrersOnly { repository => get("/:owner/:repository/branches")(referrersOnly { repository =>
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
// retrieve latest update date of each branch // retrieve latest update date of each branch
val branchInfo = repository.branchList.map { branchName => val branchInfo = repository.branchList.map { branchName =>
val revCommit = git.log.add(git.getRepository.resolve(branchName)).setMaxCount(1).call.iterator.next val revCommit = git.log.add(git.getRepository.resolve(branchName)).setMaxCount(1).call.iterator.next
(branchName, revCommit.getCommitterIdent.getWhen) (branchName, revCommit.getCommitterIdent.getWhen)
} }
repo.html.branches(branchInfo, repository) repo.html.branches(branchInfo, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
} }
}) })
/**
* Deletes branch.
*/
get("/:owner/:repository/delete/:branchName")(collaboratorsOnly { repository =>
val branchName = params("branchName")
val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.branchDelete().setBranchNames(branchName).call()
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
}
}
redirect(s"/${repository.owner}/${repository.name}/branches")
})
/** /**
* Displays tags. * Displays tags.
*/ */
@@ -165,7 +182,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
*/ */
get("/:owner/:repository/archive/:name")(referrersOnly { repository => get("/:owner/:repository/archive/:name")(referrersOnly { repository =>
val name = params("name") val name = params("name")
if(name.endsWith(".zip")){ if(name.endsWith(".zip")){
val revision = name.replaceFirst("\\.zip$", "") val revision = name.replaceFirst("\\.zip$", "")
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId) val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
@@ -173,26 +190,38 @@ trait RepositoryViewerControllerBase extends ControllerBase {
FileUtils.deleteDirectory(workDir) FileUtils.deleteDirectory(workDir)
} }
workDir.mkdirs workDir.mkdirs
// clone the repository val zipFile = new File(workDir, repository.name + "-" +
val cloneDir = new File(workDir, revision) (if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip")
JGitUtil.withGit(Git.cloneRepository
.setURI(getRepositoryDir(repository.owner, repository.name).toURI.toString) using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
.setDirectory(cloneDir) val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
.call){ git => using(new TreeWalk(git.getRepository)){ walk =>
val reader = walk.getObjectReader
// checkout the specified revision val objectId = new MutableObjectId
git.checkout.setName(revision).call
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" contentType = "application/octet-stream"
response.setHeader("Content-Disposition", s"attachment; filename=${zipFile.getName}")
zipFile zipFile
} else { } else {
BadRequest BadRequest
@@ -221,6 +250,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
(id, path.substring(id.length).replaceFirst("^/", "")) (id, path.substring(id.length).replaceFirst("^/", ""))
} }
private val readmeFiles = Seq("readme.md", "readme.markdown")
/** /**
* Provides HTML of the file list. * Provides HTML of the file list.
* *
@@ -233,26 +265,28 @@ trait RepositoryViewerControllerBase extends ControllerBase {
if(repository.commitCount == 0){ if(repository.commitCount == 0){
repo.html.guide(repository) repo.html.guide(repository)
} else { } else {
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head) val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head)
// get specified commit // get specified commit
JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) => JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
val revCommit = JGitUtil.getRevCommitFromId(git, objectId) defining(JGitUtil.getRevCommitFromId(git, objectId)){ revCommit =>
// get files // get files
val files = JGitUtil.getFileList(git, revision, path) val files = JGitUtil.getFileList(git, revision, path)
// process README.md // process README.md or README.markdown
val readme = files.find(_.name == "README.md").map { file => val readme = files.find { file =>
StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get) readmeFiles.contains(file.name.toLowerCase)
} }.map { file =>
file -> StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
}
repo.html.files(revision, repository, repo.html.files(revision, repository,
if(path == ".") Nil else path.split("/").toList, // current path if(path == ".") Nil else path.split("/").toList, // current path
new JGitUtil.CommitInfo(revCommit), // latest commit new JGitUtil.CommitInfo(revCommit), // latest commit
files, readme) files, readme)
}
} getOrElse NotFound } getOrElse NotFound
} }
} }
} }
} }

View File

@@ -1,17 +1,15 @@
package app package app
import util._ import util._
import ControlUtil._
import service._ import service._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
class SearchController extends SearchControllerBase class SearchController extends SearchControllerBase
with RepositoryService with AccountService with SystemSettingsService with ActivityService with RepositoryService with AccountService with ActivityService with RepositorySearchService with IssuesService with ReferrerAuthenticator
with RepositorySearchService with IssuesService
with ReferrerAuthenticator
trait SearchControllerBase extends ControllerBase { self: RepositoryService trait SearchControllerBase extends ControllerBase { self: RepositoryService
with SystemSettingsService with ActivityService with RepositorySearchService with ActivityService with RepositorySearchService with ReferrerAuthenticator =>
with ReferrerAuthenticator =>
val searchForm = mapping( val searchForm = mapping(
"query" -> trim(text(required)), "query" -> trim(text(required)),
@@ -26,25 +24,25 @@ trait SearchControllerBase extends ControllerBase { self: RepositoryService
} }
get("/:owner/:repository/search")(referrersOnly { repository => get("/:owner/:repository/search")(referrersOnly { repository =>
val query = params("q").trim defining(params("q").trim, params.getOrElse("type", "code")){ case (query, target) =>
val target = params.getOrElse("type", "code") val page = try {
val page = try { val i = params.getOrElse("page", "1").toInt
val i = params.getOrElse("page", "1").toInt if(i <= 0) 1 else i
if(i <= 0) 1 else i } catch {
} catch { case e: NumberFormatException => 1
case e: NumberFormatException => 1 }
}
target.toLowerCase match { target.toLowerCase match {
case "issue" => search.html.issues( case "issue" => search.html.issues(
searchIssues(repository.owner, repository.name, query), searchIssues(repository.owner, repository.name, query),
countFiles(repository.owner, repository.name, query), countFiles(repository.owner, repository.name, query),
query, page, repository) query, page, repository)
case _ => search.html.code( case _ => search.html.code(
searchFiles(repository.owner, repository.name, query), searchFiles(repository.owner, repository.name, query),
countIssues(repository.owner, repository.name, query), countIssues(repository.owner, repository.name, query),
query, page, repository) query, page, repository)
}
} }
}) })

View File

@@ -1,53 +0,0 @@
package app
import service._
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 redirect = params.get("redirect")
if(redirect.isDefined && redirect.get.startsWith("/")){
session.setAttribute("REDIRECT", redirect.get)
}
html.signin(loadSystemSettings())
}
post("/signin", form){ form =>
val settings = loadSystemSettings()
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("LOGIN_ACCOUNT", account)
updateLastLoginDate(account.userName)
session.get("REDIRECT").map { redirectUrl =>
session.removeAttribute("REDIRECT")
redirect(redirectUrl.asInstanceOf[String])
}.getOrElse {
redirect("/")
}
}
}

View File

@@ -4,15 +4,15 @@ import service.{AccountService, SystemSettingsService}
import SystemSettingsService._ import SystemSettingsService._
import util.AdminAuthenticator import util.AdminAuthenticator
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.scalatra.FlashMapSupport
class SystemSettingsController extends SystemSettingsControllerBase class SystemSettingsController extends SystemSettingsControllerBase
with SystemSettingsService with AccountService with AdminAuthenticator with SystemSettingsService with AccountService with AdminAuthenticator
trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport { trait SystemSettingsControllerBase extends ControllerBase {
self: SystemSettingsService with AccountService with AdminAuthenticator => self: SystemSettingsService with AccountService with AdminAuthenticator =>
private val form = mapping( private val form = mapping(
"baseUrl" -> trim(label("Base URL", optional(text()))),
"allowAccountRegistration" -> trim(label("Account registration", boolean())), "allowAccountRegistration" -> trim(label("Account registration", boolean())),
"gravatar" -> trim(label("Gravatar", boolean())), "gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())), "notification" -> trim(label("Notification", boolean())),
@@ -21,7 +21,9 @@ trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
"port" -> trim(label("SMTP Port", optional(number()))), "port" -> trim(label("SMTP Port", optional(number()))),
"user" -> trim(label("SMTP User", optional(text()))), "user" -> trim(label("SMTP User", optional(text()))),
"password" -> trim(label("SMTP Password", optional(text()))), "password" -> trim(label("SMTP Password", optional(text()))),
"ssl" -> trim(label("Enable SSL", optional(boolean()))) "ssl" -> trim(label("Enable SSL", optional(boolean()))),
"fromAddress" -> trim(label("FROM Address", optional(text()))),
"fromName" -> trim(label("FROM Name", optional(text())))
)(Smtp.apply)), )(Smtp.apply)),
"ldapAuthentication" -> trim(label("LDAP", boolean())), "ldapAuthentication" -> trim(label("LDAP", boolean())),
"ldap" -> optionalIfNotChecked("ldapAuthentication", mapping( "ldap" -> optionalIfNotChecked("ldapAuthentication", mapping(
@@ -31,7 +33,10 @@ trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
"bindPassword" -> trim(label("Bind Password", optional(text()))), "bindPassword" -> trim(label("Bind Password", optional(text()))),
"baseDN" -> trim(label("Base DN", text(required))), "baseDN" -> trim(label("Base DN", text(required))),
"userNameAttribute" -> trim(label("User name attribute", text(required))), "userNameAttribute" -> trim(label("User name attribute", text(required))),
"mailAttribute" -> trim(label("Mail address 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)) )(Ldap.apply))
)(SystemSettings.apply) )(SystemSettings.apply)

View File

@@ -3,7 +3,10 @@ package app
import service._ import service._
import util.AdminAuthenticator import util.AdminAuthenticator
import util.StringUtil._ import util.StringUtil._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import util.Directory._
class UserManagementController extends UserManagementControllerBase class UserManagementController extends UserManagementControllerBase
with AccountService with RepositoryService with AdminAuthenticator with AccountService with RepositoryService with AdminAuthenticator
@@ -11,58 +14,66 @@ class UserManagementController extends UserManagementControllerBase
trait UserManagementControllerBase extends AccountManagementControllerBase { trait UserManagementControllerBase extends AccountManagementControllerBase {
self: AccountService with RepositoryService with AdminAuthenticator => self: AccountService with RepositoryService with AdminAuthenticator =>
case class NewUserForm(userName: String, password: String, mailAddress: String, isAdmin: Boolean, case class NewUserForm(userName: String, password: String, fullName: String,
mailAddress: String, isAdmin: Boolean,
url: Option[String], fileId: Option[String]) url: Option[String], fileId: Option[String])
case class EditUserForm(userName: String, password: Option[String], mailAddress: String, isAdmin: Boolean, case class EditUserForm(userName: String, password: Option[String], fullName: String,
url: Option[String], fileId: Option[String], clearImage: Boolean) 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], case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
memberNames: Option[String]) memberNames: Option[String])
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
memberNames: Option[String], clearImage: Boolean) memberNames: Option[String], clearImage: Boolean, isRemoved: Boolean)
val newUserForm = mapping( val newUserForm = mapping(
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier, uniqueUserName))), "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" , text(required, maxlength(20)))), "password" -> trim(label("Password" ,text(required, maxlength(20)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))), "fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
"isAdmin" -> trim(label("User Type" , boolean())), "mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))),
"url" -> trim(label("URL" , optional(text(maxlength(200))))), "isAdmin" -> trim(label("User Type" ,boolean())),
"fileId" -> trim(label("File ID" , optional(text()))) "url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text())))
)(NewUserForm.apply) )(NewUserForm.apply)
val editUserForm = mapping( val editUserForm = mapping(
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier))), "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))),
"password" -> trim(label("Password" , optional(text(maxlength(20))))), "password" -> trim(label("Password" ,optional(text(maxlength(20))))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))), "fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
"isAdmin" -> trim(label("User Type" , boolean())), "mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))),
"url" -> trim(label("URL" , optional(text(maxlength(200))))), "isAdmin" -> trim(label("User Type" ,boolean())),
"fileId" -> trim(label("File ID" , optional(text()))), "url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"clearImage" -> trim(label("Clear image" , boolean())) "fileId" -> trim(label("File ID" ,optional(text()))),
"clearImage" -> trim(label("Clear image" ,boolean())),
"removed" -> trim(label("Disable" ,boolean()))
)(EditUserForm.apply) )(EditUserForm.apply)
val newGroupForm = mapping( val newGroupForm = mapping(
"groupName" -> trim(label("Group name" , text(required, maxlength(100), identifier))), "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
"url" -> trim(label("URL" , optional(text(maxlength(200))))), "url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))), "fileId" -> trim(label("File ID" ,optional(text()))),
"memberNames" -> trim(label("Member Names" , optional(text()))) "memberNames" -> trim(label("Member Names" ,optional(text())))
)(NewGroupForm.apply) )(NewGroupForm.apply)
val editGroupForm = mapping( val editGroupForm = mapping(
"groupName" -> trim(label("Group name" , text(required, maxlength(100), identifier))), "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
"url" -> trim(label("URL" , optional(text(maxlength(200))))), "url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))), "fileId" -> trim(label("File ID" ,optional(text()))),
"memberNames" -> trim(label("Member Names" , optional(text()))), "memberNames" -> trim(label("Member Names" ,optional(text()))),
"clearImage" -> trim(label("Clear image" , boolean())) "clearImage" -> trim(label("Clear image" ,boolean())),
"removed" -> trim(label("Disable" ,boolean()))
)(EditGroupForm.apply) )(EditGroupForm.apply)
get("/admin/users")(adminOnly { get("/admin/users")(adminOnly {
val users = getAllUsers() val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
val users = getAllUsers(includeRemoved)
val members = users.collect { case account if(account.isGroupAccount) => val members = users.collect { case account if(account.isGroupAccount) =>
account.userName -> getGroupMembers(account.userName) account.userName -> getGroupMembers(account.userName)
}.toMap }.toMap
admin.users.html.list(users, members) admin.users.html.list(users, members, includeRemoved)
}) })
get("/admin/users/_newuser")(adminOnly { get("/admin/users/_newuser")(adminOnly {
@@ -70,24 +81,39 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
}) })
post("/admin/users/_newuser", newUserForm)(adminOnly { form => post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
createAccount(form.userName, sha1(form.password), form.mailAddress, form.isAdmin, form.url) createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url)
updateImage(form.userName, form.fileId, false) updateImage(form.userName, form.fileId, false)
redirect("/admin/users") redirect("/admin/users")
}) })
get("/admin/users/:userName/_edituser")(adminOnly { get("/admin/users/:userName/_edituser")(adminOnly {
val userName = params("userName") val userName = params("userName")
admin.users.html.user(getAccountByUserName(userName)) admin.users.html.user(getAccountByUserName(userName, true))
}) })
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form => post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName).map { account => getAccountByUserName(userName, true).map { account =>
updateAccount(getAccountByUserName(userName).get.copy(
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), password = form.password.map(sha1).getOrElse(account.password),
fullName = form.fullName,
mailAddress = form.mailAddress, mailAddress = form.mailAddress,
isAdmin = form.isAdmin, isAdmin = form.isAdmin,
url = form.url)) url = form.url,
isRemoved = form.isRemoved))
updateImage(userName, form.fileId, form.clearImage) updateImage(userName, form.fileId, form.clearImage)
redirect("/admin/users") redirect("/admin/users")
@@ -107,33 +133,47 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
}) })
get("/admin/users/:groupName/_editgroup")(adminOnly { get("/admin/users/:groupName/_editgroup")(adminOnly {
val groupName = params("groupName") defining(params("groupName")){ groupName =>
admin.users.html.group(getAccountByUserName(groupName), getGroupMembers(groupName)) admin.users.html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
}
}) })
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form => post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
val groupName = params("groupName") defining(params("groupName"), form.memberNames.map(_.split(",").toList).getOrElse(Nil)){ case (groupName, memberNames) =>
getAccountByUserName(groupName).map { account => getAccountByUserName(groupName, true).map { account =>
updateGroup(groupName, form.url) updateGroup(groupName, form.url, form.isRemoved)
val memberNames = form.memberNames.map(_.split(",").toList).getOrElse(Nil) if(form.isRemoved){
updateGroupMembers(form.groupName, memberNames) // Remove from GROUP_MEMBER
updateGroupMembers(form.groupName, Nil)
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => // Remove repositories
removeCollaborators(form.groupName, repositoryName) getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
memberNames.foreach { userName => deleteRepository(groupName, repositoryName)
addCollaborator(form.groupName, repositoryName, userName) 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) updateImage(form.groupName, form.fileId, form.clearImage)
redirect("/admin/users") redirect("/admin/users")
} getOrElse NotFound } getOrElse NotFound
}
}) })
post("/admin/users/_usercheck")(adminOnly { post("/admin/users/_usercheck")(adminOnly {
getAccountByUserName(params("userName")).isDefined getAccountByUserName(params("userName")).isDefined
}) })
} }

View File

@@ -1,32 +1,37 @@
package app package app
import service._ import service._
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, JGitUtil, StringUtil} import util._
import util.Directory._ import util.Directory._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.api.Git
import org.scalatra.i18n.Messages
import scala.Some
import java.util.ResourceBundle
class WikiController extends WikiControllerBase class WikiController extends WikiControllerBase
with WikiService with RepositoryService with AccountService with ActivityService with WikiService with RepositoryService with AccountService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator
with CollaboratorsAuthenticator with ReferrerAuthenticator
trait WikiControllerBase extends ControllerBase { trait WikiControllerBase extends ControllerBase {
self: WikiService with RepositoryService with ActivityService self: WikiService with RepositoryService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator =>
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( val newForm = mapping(
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename, unique))), "pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename, unique))),
"content" -> trim(label("Content" , text(required))), "content" -> trim(label("Content" , text(required, conflictForNew))),
"message" -> trim(label("Message" , optional(text()))), "message" -> trim(label("Message" , optional(text()))),
"currentPageName" -> trim(label("Current page name" , text())) "currentPageName" -> trim(label("Current page name" , text())),
"id" -> trim(label("Latest commit id" , text()))
)(WikiPageEditForm.apply) )(WikiPageEditForm.apply)
val editForm = mapping( val editForm = mapping(
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename))), "pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename))),
"content" -> trim(label("Content" , text(required))), "content" -> trim(label("Content" , text(required, conflictForEdit))),
"message" -> trim(label("Message" , optional(text()))), "message" -> trim(label("Message" , optional(text()))),
"currentPageName" -> trim(label("Current page name" , text(required))) "currentPageName" -> trim(label("Current page name" , text(required))),
"id" -> trim(label("Latest commit id" , text(required)))
)(WikiPageEditForm.apply) )(WikiPageEditForm.apply)
get("/:owner/:repository/wiki")(referrersOnly { repository => get("/:owner/:repository/wiki")(referrersOnly { repository =>
@@ -40,13 +45,13 @@ trait WikiControllerBase extends ControllerBase {
getWikiPage(repository.owner, repository.name, pageName).map { page => getWikiPage(repository.owner, repository.name, pageName).map { page =>
wiki.html.page(pageName, page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount)) wiki.html.page(pageName, page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${pageName}/_edit") // TODO URLEncode } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
}) })
get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository => get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository =>
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match { JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match {
case Right((logs, hasNext)) => wiki.html.history(Some(pageName), logs, repository) case Right((logs, hasNext)) => wiki.html.history(Some(pageName), logs, repository)
case Left(_) => NotFound case Left(_) => NotFound
@@ -56,36 +61,60 @@ trait WikiControllerBase extends ControllerBase {
get("/:owner/:repository/wiki/:page/_compare/:commitId")(referrersOnly { repository => get("/:owner/:repository/wiki/:page/_compare/:commitId")(referrersOnly { repository =>
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
val commitId = params("commitId").split("\\.\\.\\.") val Array(from, to) = params("commitId").split("\\.\\.\\.")
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
wiki.html.compare(Some(pageName), JGitUtil.getDiffs(git, commitId(0), commitId(1), true), repository) 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 => 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 => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
wiki.html.compare(None, JGitUtil.getDiffs(git, commitId(0), commitId(1), true), repository) 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 => get("/:owner/:repository/wiki/:page/_edit")(collaboratorsOnly { repository =>
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
wiki.html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository) wiki.html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
}) })
post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) => post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) =>
val loginAccount = context.loginAccount.get defining(context.loginAccount.get){ loginAccount =>
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName, form.content, loginAccount, form.message.getOrElse(""), Some(form.id)).map { commitId =>
form.content, loginAccount, form.message.getOrElse("")).map { commitId => updateLastActivityDate(repository.owner, repository.name)
updateLastActivityDate(repository.owner, repository.name) recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId) }
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
} }
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
}) })
get("/:owner/:repository/wiki/_new")(collaboratorsOnly { get("/:owner/:repository/wiki/_new")(collaboratorsOnly {
@@ -93,25 +122,26 @@ trait WikiControllerBase extends ControllerBase {
}) })
post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) => post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) =>
val loginAccount = context.loginAccount.get defining(context.loginAccount.get){ loginAccount =>
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName, form.content, loginAccount, form.message.getOrElse(""), None)
form.content, context.loginAccount.get, form.message.getOrElse(""))
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)}") 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 => get("/:owner/:repository/wiki/:page/_delete")(collaboratorsOnly { repository =>
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
val account = context.loginAccount.get
deleteWikiPage(repository.owner, repository.name, pageName, account.userName, account.mailAddress, s"Delete ${pageName}")
updateLastActivityDate(repository.owner, repository.name)
redirect(s"/${repository.owner}/${repository.name}/wiki") 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 => get("/:owner/:repository/wiki/_pages")(referrersOnly { repository =>
@@ -120,7 +150,7 @@ trait WikiControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/wiki/_history")(referrersOnly { repository => get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, "master") match { JGitUtil.getCommitLog(git, "master") match {
case Right((logs, hasNext)) => wiki.html.history(None, logs, repository) case Right((logs, hasNext)) => wiki.html.history(None, logs, repository)
case Left(_) => NotFound case Left(_) => NotFound
@@ -129,19 +159,21 @@ trait WikiControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository => get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository =>
getFileContent(repository.owner, repository.name, multiParams("splat").head).map { content => val path = multiParams("splat").head
contentType = "application/octet-stream"
content getFileContent(repository.owner, repository.name, path).map { bytes =>
contentType = FileUtil.getContentType(path, bytes)
bytes
} getOrElse NotFound } getOrElse NotFound
}) })
private def unique: Constraint = new Constraint(){ private def unique: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, 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.") getWikiPageList(params("owner"), params("repository")).find(_ == value).map(_ => "Page already exists.")
} }
private def pagename: Constraint = new Constraint(){ private def pagename: Constraint = new Constraint(){
override def validate(name: String, value: String): Option[String] = override def validate(name: String, value: String, messages: Messages): Option[String] =
if(value.exists("\\/:*?\"<>|".contains(_))){ if(value.exists("\\/:*?\"<>|".contains(_))){
Some(s"${name} contains invalid character.") Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){ } else if(value.startsWith("_") || value.startsWith("-")){
@@ -151,5 +183,22 @@ trait WikiControllerBase extends ControllerBase {
} }
} }
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") { object Accounts extends Table[Account]("ACCOUNT") {
def userName = column[String]("USER_NAME", O PrimaryKey) def userName = column[String]("USER_NAME", O PrimaryKey)
def fullName = column[String]("FULL_NAME")
def mailAddress = column[String]("MAIL_ADDRESS") def mailAddress = column[String]("MAIL_ADDRESS")
def password = column[String]("PASSWORD") def password = column[String]("PASSWORD")
def isAdmin = column[Boolean]("ADMINISTRATOR") def isAdmin = column[Boolean]("ADMINISTRATOR")
@@ -13,11 +14,13 @@ object Accounts extends Table[Account]("ACCOUNT") {
def lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE") def lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE")
def image = column[String]("IMAGE") def image = column[String]("IMAGE")
def groupAccount = column[Boolean]("GROUP_ACCOUNT") def groupAccount = column[Boolean]("GROUP_ACCOUNT")
def * = userName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? ~ groupAccount <> (Account, Account.unapply _) def removed = column[Boolean]("REMOVED")
def * = userName ~ fullName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? ~ groupAccount ~ removed <> (Account, Account.unapply _)
} }
case class Account( case class Account(
userName: String, userName: String,
fullName: String,
mailAddress: String, mailAddress: String,
password: String, password: String,
isAdmin: Boolean, isAdmin: Boolean,
@@ -26,5 +29,6 @@ case class Account(
updatedDate: java.util.Date, updatedDate: java.util.Date,
lastLoginDate: Option[java.util.Date], lastLoginDate: Option[java.util.Date],
image: Option[String], image: Option[String],
isGroupAccount: Boolean isGroupAccount: Boolean,
isRemoved: Boolean
) )

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

@@ -9,9 +9,12 @@ import model.GroupMember
import scala.Some import scala.Some
import model.Account import model.Account
import util.LDAPUtil import util.LDAPUtil
import org.slf4j.LoggerFactory
trait AccountService { trait AccountService {
private val logger = LoggerFactory.getLogger(classOf[AccountService])
def authenticate(settings: SystemSettings, userName: String, password: String): Option[Account] = def authenticate(settings: SystemSettings, userName: String, password: String): Option[Account] =
if(settings.ldapAuthentication){ if(settings.ldapAuthentication){
ldapAuthentication(settings, userName, password) ldapAuthentication(settings, userName, password)
@@ -33,30 +36,43 @@ trait AccountService {
*/ */
private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) = { private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) = {
LDAPUtil.authenticate(settings.ldap.get, userName, password) match { LDAPUtil.authenticate(settings.ldap.get, userName, password) match {
case Right(mailAddress) => { case Right(ldapUserInfo) => {
// Create or update account by LDAP information // Create or update account by LDAP information
getAccountByUserName(userName) match { getAccountByUserName(userName, true) match {
case Some(x) => updateAccount(x.copy(mailAddress = mailAddress)) case Some(x) if(!x.isRemoved) => updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName))
case None => createAccount(userName, "", mailAddress, false, None) 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) getAccountByUserName(userName)
} }
case Left(errorMessage) => defaultAuthentication(userName, password) case Left(errorMessage) => {
logger.info(s"LDAP Authentication Failed: ${errorMessage}")
defaultAuthentication(userName, password)
}
} }
} }
def getAccountByUserName(userName: String): Option[Account] = def getAccountByUserName(userName: String, includeRemoved: Boolean = false): Option[Account] =
Query(Accounts) filter(_.userName is userName.bind) firstOption Query(Accounts) filter(t => (t.userName is userName.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
def getAccountByMailAddress(mailAddress: String): Option[Account] = def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false): Option[Account] =
Query(Accounts) filter(_.mailAddress is mailAddress.bind) firstOption Query(Accounts) filter(t => (t.mailAddress is mailAddress.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
def getAllUsers(): List[Account] = Query(Accounts) sortBy(_.userName) list 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( Accounts insert Account(
userName = userName, userName = userName,
password = password, password = password,
fullName = fullName,
mailAddress = mailAddress, mailAddress = mailAddress,
isAdmin = isAdmin, isAdmin = isAdmin,
url = url, url = url,
@@ -64,20 +80,23 @@ trait AccountService {
updatedDate = currentDate, updatedDate = currentDate,
lastLoginDate = None, lastLoginDate = None,
image = None, image = None,
isGroupAccount = false) isGroupAccount = false,
isRemoved = false)
def updateAccount(account: Account): Unit = def updateAccount(account: Account): Unit =
Accounts Accounts
.filter { a => a.userName is account.userName.bind } .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 ( .update (
account.password, account.password,
account.fullName,
account.mailAddress, account.mailAddress,
account.isAdmin, account.isAdmin,
account.url, account.url,
account.registeredDate, account.registeredDate,
currentDate, currentDate,
account.lastLoginDate) account.lastLoginDate,
account.isRemoved)
def updateAvatarImage(userName: String, image: Option[String]): Unit = def updateAvatarImage(userName: String, image: Option[String]): Unit =
Accounts.filter(_.userName is userName.bind).map(_.image.?).update(image) Accounts.filter(_.userName is userName.bind).map(_.image.?).update(image)
@@ -89,6 +108,7 @@ trait AccountService {
Accounts insert Account( Accounts insert Account(
userName = groupName, userName = groupName,
password = "", password = "",
fullName = groupName,
mailAddress = groupName + "@devnull", mailAddress = groupName + "@devnull",
isAdmin = false, isAdmin = false,
url = url, url = url,
@@ -96,10 +116,11 @@ trait AccountService {
updatedDate = currentDate, updatedDate = currentDate,
lastLoginDate = None, lastLoginDate = None,
image = None, image = None,
isGroupAccount = true) isGroupAccount = true,
isRemoved = false)
def updateGroup(groupName: String, url: Option[String]): Unit = def updateGroup(groupName: String, url: Option[String], removed: Boolean): Unit =
Accounts.filter(_.userName is groupName.bind).map(_.url.?).update(url) Accounts.filter(_.userName is groupName.bind).map(t => t.url.? ~ t.removed).update(url, removed)
def updateGroupMembers(groupName: String, members: List[String]): Unit = { def updateGroupMembers(groupName: String, members: List[String]): Unit = {
Query(GroupMembers).filter(_.groupName is groupName.bind).delete Query(GroupMembers).filter(_.groupName is groupName.bind).delete
@@ -122,4 +143,12 @@ trait AccountService {
.map(_.groupName) .map(_.groupName)
.list .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

@@ -109,11 +109,26 @@ trait ActivityService {
s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]", s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]",
None, None,
currentDate) currentDate)
def recordDeleteTagActivity(userName: String, repositoryName: String, activityUserName: String,
tagName: String, commits: List[util.JGitUtil.CommitInfo]) =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"delete_tag",
s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]",
None,
currentDate)
def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) = def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) =
Activities.autoInc insert(userName, repositoryName, activityUserName, Activities.autoInc insert(userName, repositoryName, activityUserName,
"create_branch", "create_branch",
s"[user:${activityUserName}] created branch [tag:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]", 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, None,
currentDate) currentDate)
@@ -141,7 +156,13 @@ trait ActivityService {
def insertCommitId(userName: String, repositoryName: String, commitId: String) = { def insertCommitId(userName: String, repositoryName: String, commitId: String) = {
CommitLog insert (userName, repositoryName, commitId) 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 = def existsCommitId(userName: String, repositoryName: String, commitId: String): Boolean =
Query(CommitLog).filter(_.byPrimaryKey(userName, repositoryName, commitId)).firstOption.isDefined Query(CommitLog).filter(_.byPrimaryKey(userName, repositoryName, commitId)).firstOption.isDefined

View File

@@ -49,12 +49,8 @@ trait IssuesService {
* @return the count of the search result * @return the count of the search result
*/ */
def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
repos: (String, String)*): Int = { repos: (String, String)*): Int =
// TODO It must be _.length instead of map (_.issueId) list).length. Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first
// But it does not work on Slick 1.0.1 (worked on Slick 1.0.0).
// https://github.com/slick/slick/issues/170
(searchIssueQuery(repos, condition, filterUser, onlyPullRequest) map (_.issueId) list).length
}
/** /**
* Returns the Map which contains issue count for each labels. * Returns the Map which contains issue count for each labels.
* *
@@ -253,6 +249,9 @@ trait IssuesService {
} }
.update (content, currentDate) .update (content, currentDate)
def deleteComment(commentId: Int) =
IssueComments filter (_.byPrimaryKey(commentId)) delete
def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean) = def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean) =
Issues Issues
.filter (_.byPrimaryKey(owner, repository, issueId)) .filter (_.byPrimaryKey(owner, repository, issueId))
@@ -332,7 +331,7 @@ object IssuesService {
def toURL: String = def toURL: String =
"?" + List( "?" + List(
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(" "))), if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
milestoneId.map { id => "milestone=" + (id match { milestoneId.map { id => "milestone=" + (id match {
case Some(x) => x.toString case Some(x) => x.toString
case None => "none" case None => "none"
@@ -353,11 +352,11 @@ object IssuesService {
def apply(request: HttpServletRequest): IssueSearchCondition = def apply(request: HttpServletRequest): IssueSearchCondition =
IssueSearchCondition( IssueSearchCondition(
param(request, "labels").map(_.split(" ").toSet).getOrElse(Set.empty), param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
param(request, "milestone").map(_ match { param(request, "milestone").map{
case "none" => None case "none" => None
case x => Some(x.toInt) case x => x.toIntOpt
}), },
param(request, "for"), param(request, "for"),
param(request, "state", Seq("open", "closed")).getOrElse("open"), param(request, "state", Seq("open", "closed")).getOrElse("open"),
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"), param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),

View File

@@ -2,21 +2,21 @@ package service
import scala.slick.driver.H2Driver.simple._ import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession import Database.threadLocalSession
import model._ import model._
import util.ControlUtil._
trait PullRequestService { self: IssuesService => trait PullRequestService { self: IssuesService =>
import PullRequestService._ import PullRequestService._
def getPullRequest(owner: String, repository: String, issueId: Int): Option[(Issue, PullRequest)] = { def getPullRequest(owner: String, repository: String, issueId: Int): Option[(Issue, PullRequest)] =
val issue = getIssue(owner, repository, issueId.toString) getIssue(owner, repository, issueId.toString).flatMap{ issue =>
if(issue.isDefined){ Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).firstOption.map{
Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).firstOption match { pullreq => (issue, pullreq)
case Some(pullreq) => Some((issue.get, pullreq))
case None => None
} }
} else None }
}
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] = def getPullRequestCountGroupByUser(closed: Boolean, owner: String, repository: Option[String]): List[PullRequestCount] =
Query(PullRequests) Query(PullRequests)
@@ -46,6 +46,18 @@ trait PullRequestService { self: IssuesService =>
commitIdFrom, commitIdFrom,
commitIdTo)) 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 { object PullRequestService {
@@ -54,4 +66,4 @@ object PullRequestService {
case class PullRequestCount(userName: String, count: Int) case class PullRequestCount(userName: String, count: Int)
} }

View File

@@ -2,6 +2,7 @@ package service
import util.{FileUtil, StringUtil, JGitUtil} import util.{FileUtil, StringUtil, JGitUtil}
import util.Directory._ import util.Directory._
import util.ControlUtil._
import model.Issue import model.Issue
import org.eclipse.jgit.revwalk.RevWalk import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.TreeWalk import org.eclipse.jgit.treewalk.TreeWalk
@@ -9,7 +10,8 @@ import scala.collection.mutable.ListBuffer
import org.eclipse.jgit.lib.FileMode import org.eclipse.jgit.lib.FileMode
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
trait RepositorySearchService { self: IssuesService => trait
RepositorySearchService { self: IssuesService =>
import RepositorySearchService._ import RepositorySearchService._
def countIssues(owner: String, repository: String, query: String): Int = def countIssues(owner: String, repository: String, query: String): Int =
@@ -27,12 +29,12 @@ trait RepositorySearchService { self: IssuesService =>
} }
def countFiles(owner: String, repository: String, query: String): Int = def countFiles(owner: String, repository: String, query: String): Int =
JGitUtil.withGit(getRepositoryDir(owner, repository)){ git => using(Git.open(getRepositoryDir(owner, repository))){ git =>
if(JGitUtil.isEmpty(git)) 0 else searchRepositoryFiles(git, query).length if(JGitUtil.isEmpty(git)) 0 else searchRepositoryFiles(git, query).length
} }
def searchFiles(owner: String, repository: String, query: String): List[FileSearchResult] = def searchFiles(owner: String, repository: String, query: String): List[FileSearchResult] =
JGitUtil.withGit(getRepositoryDir(owner, repository)){ git => using(Git.open(getRepositoryDir(owner, repository))){ git =>
if(JGitUtil.isEmpty(git)){ if(JGitUtil.isEmpty(git)){
Nil Nil
} else { } else {

View File

@@ -39,6 +39,67 @@ trait RepositoryService { self: AccountService =>
IssueId insert (userName, repositoryName, 0) 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 = { def deleteRepository(userName: String, repositoryName: String): Unit = {
Activities .filter(_.byRepository(userName, repositoryName)).delete Activities .filter(_.byRepository(userName, repositoryName)).delete
CommitLog .filter(_.byRepository(userName, repositoryName)).delete CommitLog .filter(_.byRepository(userName, repositoryName)).delete
@@ -46,9 +107,11 @@ trait RepositoryService { self: AccountService =>
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
Labels .filter(_.byRepository(userName, repositoryName)).delete Labels .filter(_.byRepository(userName, repositoryName)).delete
IssueComments .filter(_.byRepository(userName, repositoryName)).delete IssueComments .filter(_.byRepository(userName, repositoryName)).delete
PullRequests .filter(_.byRepository(userName, repositoryName)).delete
Issues .filter(_.byRepository(userName, repositoryName)).delete Issues .filter(_.byRepository(userName, repositoryName)).delete
IssueId .filter(_.byRepository(userName, repositoryName)).delete IssueId .filter(_.byRepository(userName, repositoryName)).delete
Milestones .filter(_.byRepository(userName, repositoryName)).delete Milestones .filter(_.byRepository(userName, repositoryName)).delete
WebHooks .filter(_.byRepository(userName, repositoryName)).delete
Repositories .filter(_.byRepository(userName, repositoryName)).delete Repositories .filter(_.byRepository(userName, repositoryName)).delete
} }
@@ -118,7 +181,7 @@ trait RepositoryService { self: AccountService =>
case Some(x) if(x.isAdmin) => Query(Repositories) case Some(x) if(x.isAdmin) => Query(Repositories)
// for Normal Users // for Normal Users
case Some(x) if(!x.isAdmin) => case Some(x) if(!x.isAdmin) =>
Query(Repositories) filter { t => (t.isPrivate is false.bind) || 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) (Query(Collaborators).filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)} exists)
} }
// for Guests // for Guests
@@ -199,20 +262,17 @@ trait RepositoryService { self: AccountService =>
} }
} }
// TODO It must be _.length instead of map (_.issueId) list).length.
// But it does not work on Slick 1.0.1 (worked on Slick 1.0.0).
// https://github.com/slick/slick/issues/170
private def getForkedCount(userName: String, repositoryName: String): Int = private def getForkedCount(userName: String, repositoryName: String): Int =
Query(Repositories).filter { t => Query(Repositories.filter { t =>
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind) (t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
}.list.length }.length).first
def getForkedRepositories(userName: String, repositoryName: String): List[String] = def getForkedRepositories(userName: String, repositoryName: String): List[(String, String)] =
Query(Repositories).filter { t => Query(Repositories).filter { t =>
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind) (t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
} }
.sortBy(_.userName asc).map(_.userName).list .sortBy(_.userName asc).map(t => t.userName ~ t.repositoryName).list
} }

View File

@@ -28,5 +28,10 @@ trait RequestCache {
} }
} }
def getAccountByMailAddress(mailAddress: String)(implicit context: app.Context): Option[Account] = {
context.cache(s"account.${mailAddress}"){
new AccountService {}.getAccountByMailAddress(mailAddress)
}
}
} }

View File

@@ -1,73 +1,95 @@
package service package service
import util.Directory._ import util.Directory._
import util.ControlUtil._
import SystemSettingsService._ import SystemSettingsService._
import javax.servlet.http.HttpServletRequest
trait SystemSettingsService { trait SystemSettingsService {
def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings().baseUrl.getOrElse {
defining(request.getRequestURL.toString){ url =>
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
}
}.replaceFirst("/$", "")
def saveSystemSettings(settings: SystemSettings): Unit = { def saveSystemSettings(settings: SystemSettings): Unit = {
val props = new java.util.Properties() defining(new java.util.Properties()){ props =>
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString) settings.baseUrl.foreach(props.setProperty(BaseURL, _))
props.setProperty(Gravatar, settings.gravatar.toString) props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
props.setProperty(Notification, settings.notification.toString) props.setProperty(Gravatar, settings.gravatar.toString)
if(settings.notification) { props.setProperty(Notification, settings.notification.toString)
settings.smtp.foreach { smtp => if(settings.notification) {
props.setProperty(SmtpHost, smtp.host) settings.smtp.foreach { smtp =>
smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString)) props.setProperty(SmtpHost, smtp.host)
smtp.user.foreach(props.setProperty(SmtpUser, _)) smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString))
smtp.password.foreach(props.setProperty(SmtpPassword, _)) smtp.user.foreach(props.setProperty(SmtpUser, _))
smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString)) 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)
props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString) if(settings.ldapAuthentication){
if(settings.ldapAuthentication){ settings.ldap.map { ldap =>
settings.ldap.map { ldap => props.setProperty(LdapHost, ldap.host)
props.setProperty(LdapHost, ldap.host) ldap.port.foreach(x => props.setProperty(LdapPort, x.toString))
ldap.port.foreach(x => props.setProperty(LdapPort, x.toString)) ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x))
ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x)) ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x)) props.setProperty(LdapBaseDN, ldap.baseDN)
props.setProperty(LdapBaseDN, ldap.baseDN) props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute) ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute) 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)
} }
props.store(new java.io.FileOutputStream(GitBucketConf), null)
} }
def loadSystemSettings(): SystemSettings = { def loadSystemSettings(): SystemSettings = {
val props = new java.util.Properties() defining(new java.util.Properties()){ props =>
if(GitBucketConf.exists){ if(GitBucketConf.exists){
props.load(new java.io.FileInputStream(GitBucketConf)) props.load(new java.io.FileInputStream(GitBucketConf))
}
SystemSettings(
getValue(props, AllowAccountRegistration, false),
getValue(props, Gravatar, true),
getValue(props, Notification, false),
if(getValue(props, Notification, false)){
Some(Smtp(
getValue(props, SmtpHost, ""),
getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
getOptionValue(props, SmtpUser, None),
getOptionValue(props, SmtpPassword, None),
getOptionValue[Boolean](props, SmtpSsl, None)))
} else {
None
},
getValue(props, LdapAuthentication, false),
if(getValue(props, LdapAuthentication, false)){
Some(Ldap(
getValue(props, LdapHost, ""),
getOptionValue(props, LdapPort, Some(DefaultLdapPort)),
getOptionValue(props, LdapBindDN, None),
getOptionValue(props, LdapBindPassword, None),
getValue(props, LdapBaseDN, ""),
getValue(props, LdapUserNameAttribute, ""),
getValue(props, LdapMailAddressAttribute, "")))
} else {
None
} }
) SystemSettings(
getOptionValue(props, BaseURL, None),
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
}
)
}
} }
} }
@@ -76,6 +98,7 @@ object SystemSettingsService {
import scala.reflect.ClassTag import scala.reflect.ClassTag
case class SystemSettings( case class SystemSettings(
baseUrl: Option[String],
allowAccountRegistration: Boolean, allowAccountRegistration: Boolean,
gravatar: Boolean, gravatar: Boolean,
notification: Boolean, notification: Boolean,
@@ -90,18 +113,24 @@ object SystemSettingsService {
bindPassword: Option[String], bindPassword: Option[String],
baseDN: String, baseDN: String,
userNameAttribute: String, userNameAttribute: String,
mailAttribute: String) fullNameAttribute: Option[String],
mailAttribute: String,
tls: Option[Boolean],
keystore: Option[String])
case class Smtp( case class Smtp(
host: String, host: String,
port: Option[Int], port: Option[Int],
user: Option[String], user: Option[String],
password: Option[String], password: Option[String],
ssl: Option[Boolean]) ssl: Option[Boolean],
fromAddress: Option[String],
fromName: Option[String])
val DefaultSmtpPort = 25 val DefaultSmtpPort = 25
val DefaultLdapPort = 389 val DefaultLdapPort = 389
private val BaseURL = "base_url"
private val AllowAccountRegistration = "allow_account_registration" private val AllowAccountRegistration = "allow_account_registration"
private val Gravatar = "gravatar" private val Gravatar = "gravatar"
private val Notification = "notification" private val Notification = "notification"
@@ -110,6 +139,8 @@ object SystemSettingsService {
private val SmtpUser = "smtp.user" private val SmtpUser = "smtp.user"
private val SmtpPassword = "smtp.password" private val SmtpPassword = "smtp.password"
private val SmtpSsl = "smtp.ssl" private val SmtpSsl = "smtp.ssl"
private val SmtpFromAddress = "smtp.from_address"
private val SmtpFromName = "smtp.from_name"
private val LdapAuthentication = "ldap_authentication" private val LdapAuthentication = "ldap_authentication"
private val LdapHost = "ldap.host" private val LdapHost = "ldap.host"
private val LdapPort = "ldap.port" private val LdapPort = "ldap.port"
@@ -117,25 +148,28 @@ object SystemSettingsService {
private val LdapBindPassword = "ldap.bind_password" private val LdapBindPassword = "ldap.bind_password"
private val LdapBaseDN = "ldap.baseDN" private val LdapBaseDN = "ldap.baseDN"
private val LdapUserNameAttribute = "ldap.username_attribute" private val LdapUserNameAttribute = "ldap.username_attribute"
private val LdapFullNameAttribute = "ldap.fullname_attribute"
private val LdapMailAddressAttribute = "ldap.mail_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 = { private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
val value = props.getProperty(key) defining(props.getProperty(key)){ value =>
if(value == null || value.isEmpty) default if(value == null || value.isEmpty) default
else convertType(value).asInstanceOf[A] else convertType(value).asInstanceOf[A]
} }
private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] = { private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] =
val value = props.getProperty(key) defining(props.getProperty(key)){ value =>
if(value == null || value.isEmpty) default if(value == null || value.isEmpty) default
else Some(convertType(value)).asInstanceOf[Option[A]] else Some(convertType(value)).asInstanceOf[Option[A]]
} }
private def convertType[A: ClassTag](value: String) = { private def convertType[A: ClassTag](value: String) =
val c = implicitly[ClassTag[A]].runtimeClass defining(implicitly[ClassTag[A]].runtimeClass){ c =>
if(c == classOf[Boolean]) value.toBoolean if(c == classOf[Boolean]) value.toBoolean
else if(c == classOf[Int]) value.toInt else if(c == classOf[Int]) value.toInt
else value 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,10 +1,21 @@
package service package service
import java.io.File
import java.util.Date import java.util.Date
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import util.{Directory, JGitUtil, LockUtil} 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 { object WikiService {
@@ -15,8 +26,9 @@ object WikiService {
* @param content the page content * @param content the page content
* @param committer the last committer * @param committer the last committer
* @param time the last modified time * @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. * The model for wiki page history.
@@ -33,29 +45,25 @@ object WikiService {
trait WikiService { trait WikiService {
import WikiService._ import WikiService._
def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit = { def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit =
LockUtil.lock(s"${owner}/${repository}/wiki"){ LockUtil.lock(s"${owner}/${repository}/wiki"){
val dir = Directory.getWikiRepositoryDir(owner, repository) defining(Directory.getWikiRepositoryDir(owner, repository)){ dir =>
if(!dir.exists){ if(!dir.exists){
try {
JGitUtil.initRepository(dir) JGitUtil.initRepository(dir)
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit") saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None)
} finally {
// once delete cloned repository because initial cloned repository does not have 'branch.master.merge'
FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner, repository))
} }
} }
} }
}
/** /**
* Returns the wiki page. * Returns the wiki page.
*/ */
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = { def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git => using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
if(!JGitUtil.isEmpty(git)){ if(!JGitUtil.isEmpty(git)){
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file => 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)
} }
} else None } else None
} }
@@ -64,8 +72,8 @@ trait WikiService {
/** /**
* Returns the content of the specified file. * Returns the content of the specified file.
*/ */
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] = { def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] =
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git => using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
if(!JGitUtil.isEmpty(git)){ if(!JGitUtil.isEmpty(git)){
val index = path.lastIndexOf('/') val index = path.lastIndexOf('/')
val parentPath = if(index < 0) "." else path.substring(0, index) val parentPath = if(index < 0) "." else path.substring(0, index)
@@ -76,58 +84,181 @@ trait WikiService {
} }
} else None } else None
} }
}
/** /**
* Returns the list of wiki page names. * Returns the list of wiki page names.
*/ */
def getWikiPageList(owner: String, repository: String): List[String] = { def getWikiPageList(owner: String, repository: String): List[String] = {
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git => using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
JGitUtil.getFileList(git, "master", ".") JGitUtil.getFileList(git, "master", ".")
.filter(_.name.endsWith(".md")) .filter(_.name.endsWith(".md"))
.map(_.name.replaceFirst("\\.md$", "")) .map(_.name.replaceFirst("\\.md$", ""))
.sortBy(x => x) .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. * Save the wiki page.
*/ */
def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String, def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
content: String, committer: model.Account, message: String): Option[String] = { content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = {
LockUtil.lock(s"${owner}/${repository}/wiki"){ LockUtil.lock(s"${owner}/${repository}/wiki"){
// clone working copy using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
val workDir = Directory.getWikiWorkDir(owner, repository) val builder = DirCache.newInCore.builder()
cloneOrPullWorkingCopy(workDir, owner, repository) val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
var created = true
var updated = false
var removed = false
// write as file if(headId != null){
JGitUtil.withGit(workDir){ git => using(new RevWalk(git.getRepository)){ revWalk =>
val file = new File(workDir, newPageName + ".md") using(new TreeWalk(git.getRepository)){ treeWalk =>
val added = if(!file.exists || FileUtils.readFileToString(file, "UTF-8") != content){ val index = treeWalk.addTree(revWalk.parseTree(headId))
FileUtils.writeStringToFile(file, content, "UTF-8") treeWalk.setRecursive(true)
git.add.addFilepattern(file.getName).call while(treeWalk.next){
true val path = treeWalk.getPathString
} else { val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
false 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 if(created || updated || removed){
val deleted = if(currentPageName != "" && currentPageName != newPageName){ builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
git.rm.addFilepattern(currentPageName + ".md").call builder.finish()
true val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
} else { if(message.trim.length == 0) {
false if(removed){
} s"Rename ${currentPageName} to ${newPageName}"
} else if(created){
s"Created ${newPageName}"
} else {
s"Updated ${newPageName}"
}
} else {
message
})
// commit and push Some(newHeadId)
if(added || deleted){ } else None
val commit = git.commit.setCommitter(committer.userName, committer.mailAddress).setMessage(message).call
git.push.call
Some(commit.getName)
} else {
None
}
} }
} }
} }
@@ -137,37 +268,35 @@ trait WikiService {
*/ */
def deleteWikiPage(owner: String, repository: String, pageName: String, def deleteWikiPage(owner: String, repository: String, pageName: String,
committer: String, mailAddress: String, message: String): Unit = { committer: String, mailAddress: String, message: String): Unit = {
LockUtil.lock(s"${owner}/${repository}/wiki"){ LockUtil.lock(s"${owner}/${repository}/wiki"){
// clone working copy using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
val workDir = Directory.getWikiWorkDir(owner, repository) val builder = DirCache.newInCore.builder()
cloneOrPullWorkingCopy(workDir, owner, repository) val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
var removed = false
// delete file using(new RevWalk(git.getRepository)){ revWalk =>
new File(workDir, pageName + ".md").delete using(new TreeWalk(git.getRepository)){ treeWalk =>
val index = treeWalk.addTree(revWalk.parseTree(headId))
JGitUtil.withGit(workDir){ git => treeWalk.setRecursive(true)
git.rm.addFilepattern(pageName + ".md").call while(treeWalk.next){
val path = treeWalk.getPathString
// commit and push val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
git.commit.setAuthor(committer, mailAddress).setMessage(message).call if(path != pageName + ".md"){
git.push.call 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)
}
}
}
} }
}
} }
private def cloneOrPullWorkingCopy(workDir: File, owner: String, repository: String): Unit = { }
if(!workDir.exists){
val git =
Git.cloneRepository
.setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString)
.setDirectory(workDir)
.call
git.getRepository.close // close .git resources.
} else {
JGitUtil.withGit(workDir){ git =>
git.pull.call
}
}
}
}

View File

@@ -1,12 +1,14 @@
package servlet package servlet
import java.io.File import java.io.File
import java.sql.Connection import java.sql.{DriverManager, Connection}
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import javax.servlet.ServletContextEvent import javax.servlet.{ServletContext, ServletContextListener, ServletContextEvent}
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import util.Directory import util.Directory._
import util.ControlUtil._
import org.eclipse.jgit.api.Git
object AutoUpdate { object AutoUpdate {
@@ -26,15 +28,14 @@ object AutoUpdate {
*/ */
def update(conn: Connection): Unit = { def update(conn: Connection): Unit = {
val sqlPath = s"update/${majorVersion}_${minorVersion}.sql" val sqlPath = s"update/${majorVersion}_${minorVersion}.sql"
val in = Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)
if(in != null){ using(Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)){ in =>
val sql = IOUtils.toString(in, "UTF-8") if(in != null){
val stmt = conn.createStatement() val sql = IOUtils.toString(in, "UTF-8")
try { using(conn.createStatement()){ stmt =>
logger.debug(sqlPath + "=" + sql) logger.debug(sqlPath + "=" + sql)
stmt.executeUpdate(sql) stmt.executeUpdate(sql)
} finally { }
stmt.close()
} }
} }
} }
@@ -49,28 +50,36 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version. * The history of versions. A head of this sequence is the current BitBucket version.
*/ */
val versions = Seq( val versions = Seq(
Version(1, 11),
Version(1, 10),
Version(1, 9),
Version(1, 8),
Version(1, 7),
Version(1, 6),
Version(1, 5), Version(1, 5),
Version(1, 4), Version(1, 4),
new Version(1, 3){ new Version(1, 3){
override def update(conn: Connection): Unit = { override def update(conn: Connection): Unit = {
super.update(conn) super.update(conn)
// Fix wiki repository configuration // Fix wiki repository configuration
val rs = conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY") using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
while(rs.next){ while(rs.next){
val wikidir = Directory.getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")) using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
val repository = org.eclipse.jgit.api.Git.open(wikidir).getRepository defining(git.getRepository.getConfig){ config =>
val config = repository.getConfig if(!config.getBoolean("http", "receivepack", false)){
if(!config.getBoolean("http", "receivepack", false)){ config.setBoolean("http", null, "receivepack", true)
config.setBoolean("http", null, "receivepack", true) config.save
config.save }
}
}
} }
repository.close
} }
} }
}, },
Version(1, 2), Version(1, 2),
Version(1, 1), Version(1, 1),
Version(1, 0) Version(1, 0),
Version(0, 0)
) )
/** /**
@@ -81,7 +90,7 @@ object AutoUpdate {
/** /**
* The version file (GITBUCKET_HOME/version). * 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. * Returns the current version from the version file.
@@ -96,46 +105,60 @@ object AutoUpdate {
} }
case _ => Version(0, 0) case _ => Version(0, 0)
} }
} else { } else Version(0, 0)
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._ import AutoUpdate._
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener]) private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
override def contextInitialized(event: ServletContextEvent): Unit = { override def contextInitialized(event: ServletContextEvent): Unit = {
event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${Directory.DatabaseHome}") val datadir = event.getServletContext.getInitParameter("gitbucket.home")
super.contextInitialized(event) if(datadir != null){
logger.debug("H2 started") System.setProperty("gitbucket.home", datadir)
}
org.h2.Driver.load()
event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true")
logger.debug("Start schema update") logger.debug("Start schema update")
val conn = getConnection() defining(getConnection(event.getServletContext)){ conn =>
try { try {
val currentVersion = getCurrentVersion() defining(getCurrentVersion()){ currentVersion =>
if(currentVersion == headVersion){ if(currentVersion == headVersion){
logger.debug("No update") logger.debug("No update")
} else { } else if(!versions.contains(currentVersion)){
versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn)) logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.")
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8") } else {
conn.commit() versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
logger.debug("Updated from " + currentVersion.versionString + " to " + headVersion.versionString) FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
} conn.commit()
} catch { logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}")
case ex: Throwable => { }
logger.error("Failed to schema update", ex) }
ex.printStackTrace() } catch {
conn.rollback() case ex: Throwable => {
logger.error("Failed to schema update", ex)
ex.printStackTrace()
conn.rollback()
}
} }
} }
logger.debug("End schema update") 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

@@ -4,6 +4,9 @@ import javax.servlet._
import javax.servlet.http._ import javax.servlet.http._
import service.{SystemSettingsService, AccountService, RepositoryService} import service.{SystemSettingsService, AccountService, RepositoryService}
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import util.Implicits._
import util.ControlUtil._
import util.Keys
/** /**
* Provides BASIC Authentication for [[servlet.GitRepositoryServlet]]. * Provides BASIC Authentication for [[servlet.GitRepositoryServlet]].
@@ -25,29 +28,30 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
} }
try { try {
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") defining(request.paths){ case Array(_, repositoryOwner, repositoryName, _*) =>
val repositoryOwner = paths(2) getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match {
val repositoryName = paths(3).replaceFirst("\\.git$", "") case Some(repository) => {
if(!request.getRequestURI.endsWith("/git-receive-pack") &&
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki", ""), "") match {
case Some(repository) => {
if(!request.getRequestURI.endsWith("/git-receive-pack") &&
!"service=git-receive-pack".equals(request.getQueryString) && !repository.repository.isPrivate){ !"service=git-receive-pack".equals(request.getQueryString) && !repository.repository.isPrivate){
chain.doFilter(req, wrappedResponse) chain.doFilter(req, wrappedResponse)
} else { } else {
request.getHeader("Authorization") match { request.getHeader("Authorization") match {
case null => requireAuth(response) case null => requireAuth(response)
case auth => decodeAuthHeader(auth).split(":") match { case auth => decodeAuthHeader(auth).split(":") match {
case Array(username, password) if(isWritableUser(username, password, repository)) => { case Array(username, password) if(isWritableUser(username, password, repository)) => {
request.setAttribute("USER_NAME", username) request.setAttribute(Keys.Request.UserName, username)
chain.doFilter(req, wrappedResponse) 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 { } catch {
case ex: Exception => { case ex: Exception => {

View File

@@ -9,8 +9,13 @@ import org.slf4j.LoggerFactory
import javax.servlet.ServletConfig import javax.servlet.ServletConfig
import javax.servlet.ServletContext import javax.servlet.ServletContext
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import util.{JGitUtil, Directory} import util.{StringUtil, Keys, JGitUtil, Directory}
import util.ControlUtil._
import util.Implicits._
import service._ import service._
import WebHookService._
import org.eclipse.jgit.api.Git
import util.JGitUtil.CommitInfo
/** /**
* Provides Git repository via HTTP. * Provides Git repository via HTTP.
@@ -38,81 +43,152 @@ class GitRepositoryServlet extends GitServlet {
def getServletContext(): ServletContext = config.getServletContext def getServletContext(): ServletContext = config.getServletContext
def getServletName(): String = config.getServletName def getServletName(): String = config.getServletName
}); })
super.init(config) super.init(config)
} }
} }
class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] { class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory]) private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory])
override def create(request: HttpServletRequest, db: Repository): ReceivePack = { override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
val receivePack = new ReceivePack(db) 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("requestURI: " + request.getRequestURI)
logger.debug("userName:" + userName) logger.debug("pusher:" + pusher)
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") defining(request.paths){ paths =>
val owner = paths(2) val owner = paths(1)
val repository = paths(3).replaceFirst("\\.git$", "") val repository = paths(2).replaceFirst("\\.git$", "")
logger.debug("repository:" + owner + "/" + repository)
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, userName)) logger.debug("repository:" + owner + "/" + repository)
receivePack
if(!repository.endsWith(".wiki")){
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseUrl(request)))
}
receivePack
}
} }
} }
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String, userName: String) extends PostReceiveHook class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String) extends PostReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService { with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook]) private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = { def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
JGitUtil.withGit(Directory.getRepositoryDir(owner, repository)) { git => try {
commands.asScala.foreach { command => using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
val commits = JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name) commands.asScala.foreach { command =>
val refName = command.getRefName.split("/") logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
val commits = command.getType match {
// apply issue comment case ReceiveCommand.Type.DELETE => Nil
val newCommits = commits.flatMap { commit => case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
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, "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 _ =>
} }
} else if(refName(1) == "tags"){ val refName = command.getRefName.split("/")
command.getType match { val branchName = refName.drop(2).mkString("/")
case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, userName, refName(2), newCommits)
// 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 _ => 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) = {
StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
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

@@ -21,9 +21,9 @@ class TransactionFilter extends Filter {
chain.doFilter(req, res) chain.doFilter(req, res)
} else { } else {
Database(req.getServletContext) withTransaction { Database(req.getServletContext) withTransaction {
logger.debug("TODO begin transaction") logger.debug("begin transaction")
chain.doFilter(req, res) chain.doFilter(req, res)
logger.debug("TODO end transaction") logger.debug("end transaction")
} }
} }
} }

View File

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

View File

@@ -0,0 +1,51 @@
package util
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.TreeWalk
import scala.util.control.Exception._
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){
allCatch {
resource.close()
}
}
}
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,15 +1,31 @@
package util package util
import java.io.File import java.io.File
import util.ControlUtil._
import org.apache.commons.io.FileUtils
/** /**
* Provides directories used by GitBucket. * Provides directories used by GitBucket.
*/ */
object Directory { object Directory {
val GitBucketHome = (scala.util.Properties.envOrNone("GITBUCKET_HOME") match { val GitBucketHome = (System.getProperty("gitbucket.home") match {
case Some(env) => new File(env) // -Dgitbucket.home=<path>
case None => new File(System.getProperty("user.home"), "gitbucket") 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 }).getAbsolutePath
val GitBucketConf = new File(GitBucketHome, "gitbucket.conf") val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
@@ -21,17 +37,17 @@ object Directory {
/** /**
* Repository names of the specified user. * Repository names of the specified user.
*/ */
def getRepositories(owner: String): List[String] = { def getRepositories(owner: String): List[String] =
val dir = new File(s"${RepositoryHome}/${owner}") defining(new File(s"${RepositoryHome}/${owner}")){ dir =>
if(dir.exists){ if(dir.exists){
dir.listFiles.filter { file => dir.listFiles.filter { file =>
file.isDirectory && !file.getName.endsWith(".wiki.git") file.isDirectory && !file.getName.endsWith(".wiki.git")
}.map(_.getName.replaceFirst("\\.git$", "")).toList }.map(_.getName.replaceFirst("\\.git$", "")).toList
} else { } else {
Nil Nil
}
} }
}
/** /**
* Substance directory of the repository. * Substance directory of the repository.
*/ */
@@ -55,25 +71,10 @@ object Directory {
def getDownloadWorkDir(owner: String, repository: String, sessionId: String): File = def getDownloadWorkDir(owner: String, repository: String, sessionId: String): File =
new File(getTemporaryDir(owner, repository), s"download/${sessionId}") new File(getTemporaryDir(owner, repository), s"download/${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")
/** /**
* Substance directory of the wiki repository. * Substance directory of the wiki repository.
*/ */
def getWikiRepositoryDir(owner: String, repository: String): File = def getWikiRepositoryDir(owner: String, repository: String): File =
new File(s"${RepositoryHome}/${owner}/${repository}.wiki.git") new File(s"${RepositoryHome}/${owner}/${repository}.wiki.git")
/**
* Wiki working directory which is cloned from the wiki repository.
*/
def getWikiWorkDir(owner: String, repository: String): File =
new File(getTemporaryDir(owner, repository), "wiki")
} }

View File

@@ -1,56 +1,72 @@
package util package util
import org.apache.commons.io.{IOUtils, FileUtils} import org.apache.commons.io.FileUtils
import java.net.URLConnection import java.net.URLConnection
import java.io.File import java.io.File
import org.apache.commons.compress.archivers.zip.{ZipArchiveEntry, ZipArchiveOutputStream} import util.ControlUtil._
object FileUtil { object FileUtil {
def getMimeType(name: String): String = { def getMimeType(name: String): String =
val fileNameMap = URLConnection.getFileNameMap() defining(URLConnection.getFileNameMap()){ fileNameMap =>
val mimeType = fileNameMap.getContentTypeFor(name) fileNameMap.getContentTypeFor(name) match {
if(mimeType == null){ case null => "application/octet-stream"
"application/octeat-stream" case mimeType => mimeType
} else { }
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 isImage(name: String): Boolean = getMimeType(name).startsWith("image/")
def isLarge(size: Long): Boolean = (size > 1024 * 1000) def isLarge(size: Long): Boolean = (size > 1024 * 1000)
def isText(content: Array[Byte]): Boolean = !content.contains(0) def isText(content: Array[Byte]): Boolean = !content.contains(0)
def createZipFile(dest: File, dir: File): Unit = { // def createZipFile(dest: File, dir: File): Unit = {
def addDirectoryToZip(out: ZipArchiveOutputStream, dir: File, path: String): Unit = { // def addDirectoryToZip(out: ZipArchiveOutputStream, dir: File, path: String): Unit = {
dir.listFiles.map { file => // dir.listFiles.map { file =>
if(file.isFile){ // if(file.isFile){
out.putArchiveEntry(new ZipArchiveEntry(path + "/" + file.getName)) // out.putArchiveEntry(new ZipArchiveEntry(path + "/" + file.getName))
out.write(FileUtils.readFileToByteArray(file)) // out.write(FileUtils.readFileToByteArray(file))
out.closeArchiveEntry // out.closeArchiveEntry
} else if(file.isDirectory){ // } else if(file.isDirectory){
addDirectoryToZip(out, file, path + "/" + file.getName) // addDirectoryToZip(out, file, path + "/" + file.getName)
} // }
} // }
// }
//
// using(new ZipArchiveOutputStream(dest)){ out =>
// addDirectoryToZip(out, dir, dir.getName)
// }
// }
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 _ => ""
} }
val out = new ZipArchiveOutputStream(dest) def withTmpDir[A](dir: File)(action: File => A): A = {
if(dir.exists()){
FileUtils.deleteDirectory(dir)
}
try { try {
addDirectoryToZip(out, dir, dir.getName) action(dir)
} finally { } finally {
IOUtils.closeQuietly(out) FileUtils.deleteDirectory(dir)
} }
} }
}
def getExtension(name: String): String = {
val index = name.lastIndexOf('.')
if(index >= 0){
name.substring(index + 1)
} else {
""
}
}
}

View File

@@ -1,6 +1,8 @@
package util package util
import scala.util.matching.Regex import scala.util.matching.Regex
import scala.util.control.Exception._
import javax.servlet.http.{HttpSession, HttpServletRequest}
/** /**
* Provides some usable implicit conversions. * Provides some usable implicit conversions.
@@ -12,7 +14,7 @@ object Implicits {
def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition) def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition)
@scala.annotation.tailrec @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 { list match {
case x :: xs => { case x :: xs => {
xs.span(condition(x, _)) match { xs.span(condition(x, _)) match {
@@ -21,7 +23,6 @@ object Implicits {
} }
case Nil => result case Nil => result
} }
}
} }
implicit class RichString(value: String){ implicit class RichString(value: String){
@@ -41,6 +42,36 @@ object Implicits {
} }
sb.toString sb.toString
} }
def toIntOpt: Option[Int] = catching(classOf[NumberFormatException]) opt {
Integer.parseInt(value)
}
} }
} 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

@@ -3,6 +3,7 @@ package util
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import util.Directory._ import util.Directory._
import util.StringUtil._ import util.StringUtil._
import util.ControlUtil._
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
import org.eclipse.jgit.lib._ import org.eclipse.jgit.lib._
import org.eclipse.jgit.revwalk._ import org.eclipse.jgit.revwalk._
@@ -14,6 +15,7 @@ import org.eclipse.jgit.errors.MissingObjectException
import java.util.Date import java.util.Date
import org.eclipse.jgit.api.errors.NoHeadException import org.eclipse.jgit.api.errors.NoHeadException
import service.RepositoryService import service.RepositoryService
import org.eclipse.jgit.dircache.DirCacheEntry
/** /**
* Provides complex JGit operations. * Provides complex JGit operations.
@@ -42,8 +44,10 @@ object JGitUtil {
* @param message the last commit message * @param message the last commit message
* @param commitId the last commit id * @param commitId the last commit id
* @param committer the last committer name * @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. * The commit data.
@@ -68,29 +72,17 @@ object JGitUtil {
rev.getFullMessage, rev.getFullMessage,
rev.getParents().map(_.name).toList) rev.getParents().map(_.name).toList)
val summary = { val summary = defining(fullMessage.trim.indexOf("\n")){ i =>
val i = fullMessage.trim.indexOf("\n") defining(if(i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage){ firstLine =>
val firstLine = if(i >= 0){ if(firstLine.length > shortMessage.length) shortMessage else firstLine
fullMessage.trim.substring(0, i).trim
} else {
fullMessage
}
if(firstLine.length > shortMessage.length){
shortMessage
} else {
firstLine
} }
} }
val description = { val description = defining(fullMessage.trim.indexOf("\n")){ i =>
val i = fullMessage.trim.indexOf("\n")
if(i >= 0){ if(i >= 0){
Some(fullMessage.trim.substring(i).trim) Some(fullMessage.trim.substring(i).trim)
} else { } else None
None
}
} }
} }
case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String]) case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String])
@@ -112,24 +104,6 @@ object JGitUtil {
*/ */
case class TagInfo(name: String, time: Date, id: String) 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 or tag id. * Returns RevCommit from the commit or tag id.
* *
@@ -141,7 +115,7 @@ object JGitUtil {
val revWalk = new RevWalk(git.getRepository) val revWalk = new RevWalk(git.getRepository)
val revCommit = revWalk.parseAny(objectId) match { val revCommit = revWalk.parseAny(objectId) match {
case r: RevTag => revWalk.parseCommit(r.getObject) case r: RevTag => revWalk.parseCommit(r.getObject)
case _ => revWalk.parseCommit(objectId) case _ => revWalk.parseCommit(objectId)
} }
revWalk.dispose revWalk.dispose
revCommit revCommit
@@ -151,7 +125,7 @@ object JGitUtil {
* Returns the repository information. It contains branch names and tag names. * Returns the repository information. It contains branch names and tag names.
*/ */
def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = { def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = {
withGit(getRepositoryDir(owner, repository)){ git => using(Git.open(getRepositoryDir(owner, repository))){ git =>
try { try {
// get commit count // get commit count
val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(1000).sum val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(1000).sum
@@ -188,45 +162,43 @@ object JGitUtil {
* @return HTML of the file list * @return HTML of the file list
*/ */
def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = { 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)] val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String)]
while (treeWalk.next()) { using(new RevWalk(git.getRepository)){ revWalk =>
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString)) 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) val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision)
list.map { case (objectId, fileMode, path, name) => list.map { case (objectId, fileMode, path, name) =>
@@ -237,7 +209,8 @@ object JGitUtil {
commits(path).getCommitterIdent.getWhen, commits(path).getCommitterIdent.getWhen,
commits(path).getShortMessage, commits(path).getShortMessage,
commits(path).getName, commits(path).getName,
commits(path).getCommitterIdent.getName) commits(path).getCommitterIdent.getName,
commits(path).getCommitterIdent.getEmailAddress)
}.sortWith { (file1, file2) => }.sortWith { (file1, file2) =>
(file1.isDirectory, file2.isDirectory) match { (file1.isDirectory, file2.isDirectory) match {
case (true , false) => true case (true , false) => true
@@ -270,25 +243,23 @@ object JGitUtil {
case _ => (logs, i.hasNext) case _ => (logs, i.hasNext)
} }
val revWalk = new RevWalk(git.getRepository) using(new RevWalk(git.getRepository)){ revWalk =>
val objectId = git.getRepository.resolve(revision) defining(git.getRepository.resolve(revision)){ objectId =>
if(objectId == null){ if(objectId == null){
Left(s"${revision} can't be resolved.") Left(s"${revision} can't be resolved.")
} else { } else {
revWalk.markStart(revWalk.parseCommit(objectId)) revWalk.markStart(revWalk.parseCommit(objectId))
if(path.nonEmpty){ if(path.nonEmpty){
revWalk.setRevFilter(new RevFilter(){ revWalk.setRevFilter(new RevFilter(){
def include(walk: RevWalk, commit: RevCommit): Boolean = { def include(walk: RevWalk, commit: RevCommit): Boolean = {
getDiffs(git, commit.getName, false)._1.find(_.newPath == path).nonEmpty getDiffs(git, commit.getName, false)._1.find(_.newPath == path).nonEmpty
}
override def clone(): RevFilter = this
})
} }
override def clone(): RevFilter = this Right(getCommitLog(revWalk.iterator, 0, Nil))
}) }
} }
val commits = getCommitLog(revWalk.iterator, 0, Nil)
revWalk.release
Right(commits)
} }
} }
@@ -308,13 +279,10 @@ object JGitUtil {
case false => logs case false => logs
} }
val revWalk = new RevWalk(git.getRepository) using(new RevWalk(git.getRepository)){ revWalk =>
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(begin))) revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(begin)))
getCommitLog(revWalk.iterator, Nil).reverse
val commits = getCommitLog(revWalk.iterator, Nil) }
revWalk.release
commits.reverse
} }
@@ -370,11 +338,8 @@ object JGitUtil {
if(large == false && FileUtil.isLarge(loader.getSize)){ if(large == false && FileUtil.isLarge(loader.getSize)){
None None
} else { } else {
val db = git.getRepository.getObjectDatabase using(git.getRepository.getObjectDatabase){ db =>
try {
Some(db.open(id).getBytes) Some(db.open(id).getBytes)
} finally {
db.close
} }
} }
} catch { } catch {
@@ -392,34 +357,32 @@ object JGitUtil {
case _ => logs case _ => logs
} }
val revWalk = new RevWalk(git.getRepository) using(new RevWalk(git.getRepository)){ revWalk =>
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(id))) revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(id)))
val commits = getCommitLog(revWalk.iterator, Nil)
val commits = getCommitLog(revWalk.iterator, Nil) val revCommit = commits(0)
revWalk.release
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 { if(commits.length >= 2){
// initial commit // not initial commit
val walk = new TreeWalk(git.getRepository) val oldCommit = commits(1)
walk.addTree(revCommit.getTree) (getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName))
val buffer = new scala.collection.mutable.ListBuffer[DiffInfo]()
while(walk.next){ } else {
buffer.append((if(!fetchContent){ // initial commit
DiffInfo(ChangeType.ADD, null, walk.getPathString, None, None) using(new TreeWalk(git.getRepository)){ treeWalk =>
} else { treeWalk.addTree(revCommit.getTree)
DiffInfo(ChangeType.ADD, null, walk.getPathString, None, val buffer = new scala.collection.mutable.ListBuffer[DiffInfo]()
JGitUtil.getContent(git, walk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray)) 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)
}
} }
walk.release
(buffer.toList, None)
} }
} }
@@ -447,76 +410,88 @@ object JGitUtil {
/** /**
* Returns the list of branch names of the specified commit. * Returns the list of branch names of the specified commit.
*/ */
def getBranchesOfCommit(git: Git, commitId: String): List[String] = { def getBranchesOfCommit(git: Git, commitId: String): List[String] =
val walk = new org.eclipse.jgit.revwalk.RevWalk(git.getRepository) using(new RevWalk(git.getRepository)){ revWalk =>
try { defining(revWalk.parseCommit(git.getRepository.resolve(commitId + "^0"))){ commit =>
val commit = walk.parseCommit(git.getRepository.resolve(commitId + "^0")) git.getRepository.getAllRefs.entrySet.asScala.filter { e =>
(e.getKey.startsWith(Constants.R_HEADS) && revWalk.isMergedInto(commit, revWalk.parseCommit(e.getValue.getObjectId)))
git.getRepository.getAllRefs.entrySet.asScala.filter { e => }.map { e =>
(e.getKey.startsWith(Constants.R_HEADS) && walk.isMergedInto(commit, walk.parseCommit(e.getValue.getObjectId))) e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_HEADS.length)
}.map { e => }.toList.sorted
e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_HEADS.length) }
}.toList.sorted
} finally {
walk.release
} }
}
/** /**
* Returns the list of tags of the specified commit. * Returns the list of tags of the specified commit.
*/ */
def getTagsOfCommit(git: Git, commitId: String): List[String] = { def getTagsOfCommit(git: Git, commitId: String): List[String] =
val walk = new org.eclipse.jgit.revwalk.RevWalk(git.getRepository) using(new RevWalk(git.getRepository)){ revWalk =>
try { defining(revWalk.parseCommit(git.getRepository.resolve(commitId + "^0"))){ commit =>
val commit = walk.parseCommit(git.getRepository.resolve(commitId + "^0")) git.getRepository.getAllRefs.entrySet.asScala.filter { e =>
(e.getKey.startsWith(Constants.R_TAGS) && revWalk.isMergedInto(commit, revWalk.parseCommit(e.getValue.getObjectId)))
git.getRepository.getAllRefs.entrySet.asScala.filter { e => }.map { e =>
(e.getKey.startsWith(Constants.R_TAGS) && walk.isMergedInto(commit, walk.parseCommit(e.getValue.getObjectId))) e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_TAGS.length)
}.map { e => }.toList.sorted.reverse
e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_TAGS.length) }
}.toList.sorted.reverse
} finally {
walk.release
} }
}
def initRepository(dir: java.io.File): Unit = { def initRepository(dir: java.io.File): Unit =
val repository = new RepositoryBuilder().setGitDir(dir).setBare.build using(new RepositoryBuilder().setGitDir(dir).setBare.build){ repository =>
try {
repository.create repository.create
setReceivePack(repository) setReceivePack(repository)
} finally {
repository.close
} }
}
def cloneRepository(from: java.io.File, to: java.io.File): Unit = { def cloneRepository(from: java.io.File, to: java.io.File): Unit =
val git = Git.cloneRepository.setURI(from.toURI.toString).setDirectory(to).setBare(true).call using(Git.cloneRepository.setURI(from.toURI.toString).setDirectory(to).setBare(true).call){ git =>
try {
setReceivePack(git.getRepository) setReceivePack(git.getRepository)
} finally {
git.getRepository.close
} }
}
def isEmpty(git: Git): Boolean = git.getRepository.resolve(Constants.HEAD) == null def isEmpty(git: Git): Boolean = git.getRepository.resolve(Constants.HEAD) == null
private def setReceivePack(repository: org.eclipse.jgit.lib.Repository): Unit = { private def setReceivePack(repository: org.eclipse.jgit.lib.Repository): Unit =
val config = repository.getConfig defining(repository.getConfig){ config =>
config.setBoolean("http", null, "receivepack", true) config.setBoolean("http", null, "receivepack", true)
config.save config.save
} }
def getDefaultBranch(git: Git, repository: RepositoryService.RepositoryInfo, def getDefaultBranch(git: Git, repository: RepositoryService.RepositoryInfo,
revstr: String = ""): Option[(ObjectId, String)] = { revstr: String = ""): Option[(ObjectId, String)] = {
Seq( Seq(
if(revstr.isEmpty) repository.repository.defaultBranch else revstr, Some(if(revstr.isEmpty) repository.repository.defaultBranch else revstr),
repository.branchList.head repository.branchList.headOption
).map { rev => ).flatMap {
(git.getRepository.resolve(rev), rev) case Some(rev) => Some((git.getRepository.resolve(rev), rev))
case None => None
}.find(_._1 != null) }.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,81 @@
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 = "loginAccount"
/**
* 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}"
}
object Flash {
/**
* Flash key for the redirect URL.
*/
val Redirect = "redirect"
/**
* Flash key for the information message.
*/
val Info = "info"
}
/**
* 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

@@ -1,10 +1,11 @@
package util package util
import service.SystemSettingsService.Ldap import util.ControlUtil._
import service.SystemSettingsService import service.SystemSettingsService
import com.novell.ldap._ import com.novell.ldap._
import java.security.Security
import org.slf4j.LoggerFactory
import service.SystemSettingsService.Ldap import service.SystemSettingsService.Ldap
import scala.Some
import scala.annotation.tailrec import scala.annotation.tailrec
/** /**
@@ -12,72 +13,98 @@ import scala.annotation.tailrec
*/ */
object LDAPUtil { object LDAPUtil {
private val LDAP_VERSION: Int = 3 private val LDAP_VERSION: Int = LDAPConnection.LDAP_V3
private val logger = LoggerFactory.getLogger(getClass().getName())
/** /**
* Try authentication by LDAP using given configuration. * Try authentication by LDAP using given configuration.
* Returns Right(mailAddress) if authentication is successful, otherwise Left(errorMessage). * Returns Right(LDAPUserInfo) if authentication is successful, otherwise Left(errorMessage).
*/ */
def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, String] = { def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, LDAPUserInfo] = {
bind( bind(
ldapSettings.host, host = ldapSettings.host,
ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort), port = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
ldapSettings.bindDN.getOrElse(""), dn = ldapSettings.bindDN.getOrElse(""),
ldapSettings.bindPassword.getOrElse("") password = ldapSettings.bindPassword.getOrElse(""),
) match { tls = ldapSettings.tls.getOrElse(false),
case Some(conn) => { keystore = ldapSettings.keystore.getOrElse(""),
withConnection(conn) { conn => error = "System LDAP authentication failed."
findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match { ){ conn =>
case Some(userDN) => userAuthentication(ldapSettings, userDN, password) findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match {
case None => Left("User does not exist") case Some(userDN) => userAuthentication(ldapSettings, userDN, userName, password)
} case None => Left("User does not exist.")
}
} }
case None => Left("System LDAP authentication failed.")
} }
} }
private def userAuthentication(ldapSettings: Ldap, userDN: String, password: String): Either[String, String] = { private def userAuthentication(ldapSettings: Ldap, userDN: String, userName: String, password: String): Either[String, LDAPUserInfo] = {
bind( bind(
ldapSettings.host, host = ldapSettings.host,
ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort), port = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
userDN, dn = userDN,
password password = password,
) match { tls = ldapSettings.tls.getOrElse(false),
case Some(conn) => { keystore = ldapSettings.keystore.getOrElse(""),
withConnection(conn) { conn => error = "User LDAP Authentication Failed."
findMailAddress(conn, userDN, ldapSettings.mailAttribute) match { ){ conn =>
case Some(mailAddress) => Right(mailAddress) findMailAddress(conn, userDN, ldapSettings.mailAttribute) match {
case None => Left("Can't find mail address.") 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.")
} }
case None => Left("User LDAP Authentication Failed.")
} }
} }
private def bind(host: String, port: Int, dn: String, password: String): Option[LDAPConnection] = { private def bind[A](host: String, port: Int, dn: String, password: String, tls: Boolean, keystore: String, error: String)
val conn: LDAPConnection = new LDAPConnection (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 { try {
// Connect to the server
conn.connect(host, port) conn.connect(host, port)
if (tls) {
// Secure the connection
conn.startTLS()
}
// Bind to the server
conn.bind(LDAP_VERSION, dn, password.getBytes) conn.bind(LDAP_VERSION, dn, password.getBytes)
Some(conn)
// Execute a given function and returns a its result
f(conn)
} catch { } catch {
case e: Exception => { case e: Exception => {
if (conn.isConnected) conn.disconnect() // Provide more information if something goes wrong
None logger.info("" + e)
if (conn.isConnected) {
conn.disconnect()
}
// Returns an error message
Left(error)
} }
} }
} }
private def withConnection[T](conn: LDAPConnection)(f: LDAPConnection => T): T = { /**
try { * Search a specified user and returns userDN if exists.
f(conn) */
} finally {
conn.disconnect()
}
}
private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String): Option[String] = { private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String): Option[String] = {
@tailrec @tailrec
def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = { def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = {
@@ -96,12 +123,20 @@ object LDAPUtil {
} }
} }
private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] = { private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] =
val results = conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false) defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false)){ results =>
if (results.hasMore) { if(results.hasMore) {
Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue) Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
} else { } else None
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

@@ -2,6 +2,7 @@ package util
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.locks.{ReentrantLock, Lock} import java.util.concurrent.locks.{ReentrantLock, Lock}
import util.ControlUtil._
object LockUtil { object LockUtil {
@@ -23,8 +24,7 @@ object LockUtil {
/** /**
* Synchronizes a given function which modifies the working copy of the wiki repository. * Synchronizes a given function which modifies the working copy of the wiki repository.
*/ */
def lock[T](key: String)(f: => T): T = { def lock[T](key: String)(f: => T): T = defining(getLockObject(key)){ lock =>
val lock = getLockObject(key)
try { try {
lock.lock() lock.lock()
f f

View File

@@ -9,6 +9,7 @@ import app.Context
import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService} import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService}
import servlet.Database import servlet.Database
import SystemSettingsService.Smtp import SystemSettingsService.Smtp
import _root_.util.ControlUtil.defining
trait Notifier extends RepositoryService with AccountService with IssuesService { trait Notifier extends RepositoryService with AccountService with IssuesService {
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
@@ -65,26 +66,37 @@ class Mailer(private val smtp: Smtp) extends Notifier {
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
(msg: String => String)(implicit context: Context) = { (msg: String => String)(implicit context: Context) = {
val f = future { val database = Database(context.request.getServletContext)
val email = new HtmlEmail
email.setHostName(smtp.host)
email.setSmtpPort(smtp.port.get)
smtp.user.foreach { user =>
email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse("")))
}
smtp.ssl.foreach { ssl =>
email.setSSLOnConnect(ssl)
}
email.setFrom("notifications@gitbucket.com", context.loginAccount.get.userName)
email.setHtmlMsg(msg(view.Markdown.toHtml(content, r, false, true)))
val f = future {
// TODO Can we use the Database Session in other than Transaction Filter? // TODO Can we use the Database Session in other than Transaction Filter?
Database(context.request.getServletContext) withSession { database withSession {
getIssue(r.owner, r.name, issueId.toString) foreach { issue => getIssue(r.owner, r.name, issueId.toString) foreach { issue =>
email.setSubject(s"[${r.name}] ${issue.title} (#${issueId})") defining(
recipients(issue) { s"[${r.name}] ${issue.title} (#${issueId})" ->
email.getToAddresses.clear msg(view.Markdown.toHtml(content, r, false, true))) { case (subject, msg) =>
email.addTo(_).send 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
}
} }
} }
} }

View File

@@ -2,14 +2,17 @@ package util
import java.net.{URLDecoder, URLEncoder} import java.net.{URLDecoder, URLEncoder}
import org.mozilla.universalchardet.UniversalDetector import org.mozilla.universalchardet.UniversalDetector
import util.ControlUtil._
import org.apache.commons.io.input.BOMInputStream
import org.apache.commons.io.IOUtils
object StringUtil { object StringUtil {
def sha1(value: String): String = { def sha1(value: String): String =
val md = java.security.MessageDigest.getInstance("SHA-1") defining(java.security.MessageDigest.getInstance("SHA-1")){ md =>
md.update(value.getBytes) md.update(value.getBytes)
md.digest.map(b => "%02x".format(b)).mkString md.digest.map(b => "%02x".format(b)).mkString
} }
def md5(value: String): String = { def md5(value: String): String = {
val md = java.security.MessageDigest.getInstance("MD5") val md = java.security.MessageDigest.getInstance("MD5")
@@ -26,15 +29,30 @@ object StringUtil {
def escapeHtml(value: String): String = def escapeHtml(value: String): String =
value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;") value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
def convertFromByteArray(content: Array[Byte]): String = new String(content, detectEncoding(content)) /**
* 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 = { def detectEncoding(content: Array[Byte]): String =
val detector = new UniversalDetector(null) defining(new UniversalDetector(null)){ detector =>
detector.handleData(content, 0, content.length) detector.handleData(content, 0, content.length)
detector.dataEnd() detector.dataEnd()
detector.getDetectedCharset match { detector.getDetectedCharset match {
case null => "UTF-8" case null => "UTF-8"
case e => e case e => e
}
} }
}
/**
* Extract issue id like ````#issueId``` from the given message.
*
*@param message the message which may contains issue id
* @return the iterator of issue id
*/
def extractIssueId(message: String): Iterator[String] =
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(message).matchData.map { matchData => matchData.group(2) }
} }

View File

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

View File

@@ -13,17 +13,31 @@ trait AvatarImageProvider { self: RequestCache =>
protected def getAvatarImageHtml(userName: String, size: Int, protected def getAvatarImageHtml(userName: String, size: Int,
mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html = { mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html = {
val src = getAccountByUserName(userName).map { account => val src = if(mailAddress.isEmpty){
if(account.image.isEmpty && getSystemSettings().gravatar){ // by user name
s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress)}?s=${size}""" getAccountByUserName(userName).map { account =>
} else { if(account.image.isEmpty && getSystemSettings().gravatar){
s"""${context.path}/${userName}/_avatar""" 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"""
} }
} getOrElse { } else {
if(mailAddress.nonEmpty && getSystemSettings().gravatar){ // by mail address
s"""http://www.gravatar.com/avatar/${StringUtil.md5(mailAddress)}?s=${size}""" getAccountByMailAddress(mailAddress).map { account =>
} else { if(account.image.isEmpty && getSystemSettings().gravatar){
s"""${context.path}/${userName}/_avatar""" 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"""
}
} }
} }

View File

@@ -14,20 +14,21 @@ trait LinkConverter { self: RequestCache =>
// escape HTML tags // escape HTML tags
.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;") .replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;")
// convert issue id to link // convert issue id to link
.replaceBy(("(^|\\W)" + issueIdPrefix + "(\\d+)(\\W|$)").r){ m => .replaceBy(("(?<=(^|\\W))" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r){ m =>
if(getIssue(repository.owner, repository.name, m.group(2)).isDefined){ getIssue(repository.owner, repository.name, m.group(2)) match {
Some(s"""${m.group(1)}<a href="${context.path}/${repository.owner}/${repository.name}/issues/${m.group(2)}">#${m.group(2)}</a>${m.group(3)}""") case Some(issue) if(issue.isPullRequest)
} else { => Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/pull/${m.group(2)}">#${m.group(2)}</a>""")
Some(s"""${m.group(1)}#${m.group(2)}${m.group(3)}""") 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 // convert @username to link
.replaceBy("(^|\\W)@([a-zA-Z0-9\\-_]+)(\\W|$)".r){ m => .replaceBy("(?<=(^|\\W))@([a-zA-Z0-9\\-_]+)(?=(\\W|$))".r){ m =>
getAccountByUserName(m.group(2)).map { _ => getAccountByUserName(m.group(2)).map { _ =>
s"""${m.group(1)}<a href="${context.path}/${m.group(2)}">@${m.group(2)}</a>${m.group(3)}""" s"""<a href="${context.path}/${m.group(2)}">@${m.group(2)}</a>"""
} }
} }
// convert commit id to link // convert commit id to link
.replaceAll("(^|\\W)([a-f0-9]{40})(\\W|$)", s"""$$1<a href="${context.path}/${repository.owner}/${repository.name}/commit/$$2">$$2</a>$$3""") .replaceAll("(?<=(^|\\W))([a-f0-9]{40})(?=(\\W|$))", s"""<a href="${context.path}/${repository.owner}/${repository.name}/commit/$$2">$$2</a>""")
} }
} }

View File

@@ -1,12 +1,16 @@
package view package view
import util.StringUtil import util.StringUtil
import util.ControlUtil._
import util.Directory._
import org.parboiled.common.StringUtils import org.parboiled.common.StringUtils
import org.pegdown._ import org.pegdown._
import org.pegdown.ast._ import org.pegdown.ast._
import org.pegdown.LinkRenderer.Rendering import org.pegdown.LinkRenderer.Rendering
import java.text.Normalizer
import java.util.Locale
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
import service.RequestCache import service.{RequestCache, WikiService}
object Markdown { object Markdown {
@@ -17,11 +21,11 @@ object Markdown {
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): String = { enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): String = {
// escape issue id // escape issue id
val source = if(enableRefsLink){ val source = if(enableRefsLink){
markdown.replaceAll("(^|\\W)#([0-9]+)(\\W|$)", "$1issue:$2$3") markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2")
} else markdown } else markdown
val rootNode = new PegDownProcessor( val rootNode = new PegDownProcessor(
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS
).parseMarkdown(source.toCharArray) ).parseMarkdown(source.toCharArray)
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink).toHtml(rootNode) new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink).toHtml(rootNode)
@@ -29,7 +33,7 @@ object Markdown {
} }
class GitBucketLinkRender(context: app.Context, repository: service.RepositoryService.RepositoryInfo, 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 = { override def render(node: WikiLinkNode): Rendering = {
if(enableWikiLink){ if(enableWikiLink){
try { try {
@@ -40,8 +44,14 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
} else { } else {
(text, text) (text, text)
} }
val url = repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/" + StringUtil.urlEncode(page) val url = repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/" + StringUtil.urlEncode(page)
new Rendering(url, label)
if(getWikiPage(repository.owner, repository.name, page).isDefined){
new Rendering(url, label)
} else {
new Rendering(url, label).withAttribute("class", "absent")
}
} catch { } catch {
case e: java.io.UnsupportedEncodingException => throw new IllegalStateException case e: java.io.UnsupportedEncodingException => throw new IllegalStateException
} }
@@ -52,7 +62,7 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
} }
class GitBucketVerbatimSerializer extends VerbatimSerializer { class GitBucketVerbatimSerializer extends VerbatimSerializer {
def serialize(node: VerbatimNode, printer: Printer) { def serialize(node: VerbatimNode, printer: Printer): Unit = {
printer.println.print("<pre") printer.println.print("<pre")
if (!StringUtils.isEmpty(node.getType)) { if (!StringUtils.isEmpty(node.getType)) {
printer.print(" class=").print('"').print("prettyprint ").print(node.getType).print('"') printer.print(" class=").print('"').print("prettyprint ").print(node.getType).print('"')
@@ -98,11 +108,26 @@ 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('"') printer.print(' ').print(name).print('=').print('"').print(value).print('"')
} }
override def visit(node: TextNode) { private def printHeaderTag(node: HeaderNode): Unit = {
val tag = s"h${node.getLevel}"
val headerTextString = printChildrenToString(node)
val anchorName = GitBucketHtmlSerializer.generateAnchorName(headerTextString)
printer.print(s"""<$tag class="markdown-head">""")
printer.print(s"""<a class="markdown-anchor-link" href="#$anchorName"></a>""")
printer.print(s"""<a class="markdown-anchor" name="$anchorName"></a>""")
visitChildren(node)
printer.print(s"</$tag>")
}
override def visit(node: HeaderNode): Unit = {
printHeaderTag(node)
}
override def visit(node: TextNode): Unit = {
// convert commit id and username to link. // convert commit id and username to link.
val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText
@@ -112,5 +137,16 @@ class GitBucketHtmlSerializer(
printWithAbbreviations(text) printWithAbbreviations(text)
} }
} }
}
} object GitBucketHtmlSerializer {
private val Whitespace = "[\\s]".r
def generateAnchorName(text: String): String = {
val noWhitespace = Whitespace.replaceAllIn(text, "-")
val normalized = Normalizer.normalize(noWhitespace, Normalizer.Form.NFD)
val noSpecialChars = StringUtil.urlEncode(normalized)
noSpecialChars.toLowerCase(Locale.ENGLISH)
}
}

View File

@@ -9,12 +9,12 @@ import service.RequestCache
* Provides helper methods for Twirl templates. * Provides helper methods for Twirl templates.
*/ */
object helpers extends AvatarImageProvider with LinkConverter with RequestCache { object helpers extends AvatarImageProvider with LinkConverter with RequestCache {
/** /**
* Format java.util.Date to "yyyy-MM-dd HH:mm:ss". * 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) def datetime(date: Date): String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date)
/** /**
* Format java.util.Date to "yyyy-MM-dd". * Format java.util.Date to "yyyy-MM-dd".
*/ */
@@ -31,17 +31,20 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
* Converts Markdown of Wiki pages to HTML. * Converts Markdown of Wiki pages to HTML.
*/ */
def markdown(value: String, repository: service.RepositoryService.RepositoryInfo, def markdown(value: String, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = { enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink)) Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink))
}
/** /**
* Returns &lt;img&gt; which displays the avatar icon. * Returns &lt;img&gt; which displays the avatar icon for the given user name.
* Looks up Gravatar if avatar icon has not been configured in user settings. * 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 = def avatar(userName: String, size: Int, tooltip: Boolean = false)(implicit context: app.Context): Html =
getAvatarImageHtml(userName, size, "", tooltip) 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 = def avatar(commit: util.JGitUtil.CommitInfo, size: Int)(implicit context: app.Context): Html =
getAvatarImageHtml(commit.committer, size, commit.mailAddress) getAvatarImageHtml(commit.committer, size, commit.mailAddress)
@@ -58,16 +61,31 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
value value
} }
import scala.util.matching.Regex._
implicit class RegexReplaceString(s: String) {
def replaceAll(pattern: String, replacer: (Match) => String): String = {
pattern.r.replaceAllIn(s, replacer)
}
}
/**
* Convert link notations in the activity message.
*/
def activityMessage(message: String)(implicit context: app.Context): Html = def activityMessage(message: String)(implicit context: app.Context): Html =
Html(message Html(message
.replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/issues/$$3">$$1/$$2#$$3</a>""") .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("\\[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("\\[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("\\[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]+?)\\]" , s"""<a href="${context.path}/$$1/$$2/tree/$$3">$$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]+?)\\]" , s"""<a href="${context.path}/$$1">$$1</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: String): String = StringUtil.urlEncode(value)
def urlEncode(value: Option[String]): String = value.map(urlEncode).getOrElse("") def urlEncode(value: Option[String]): String = value.map(urlEncode).getOrElse("")
@@ -81,15 +99,40 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
/** /**
* Generates the url to the account page. * Generates the url to the account page.
*/ */
def url(userName: String)(implicit context: app.Context): String = def url(userName: String)(implicit context: app.Context): String = s"${context.path}/${userName}"
s"${context.path}/${userName}"
/** /**
* Returns the url to the root of assets. * Returns the url to the root of assets.
*/ */
def assets(implicit context: app.Context): String = def assets(implicit context: app.Context): String = s"${context.path}/assets"
s"${context.path}/assets"
/**
* 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 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 def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime
/** /**

View File

@@ -13,42 +13,52 @@
<div class="span6"> <div class="span6">
@if(account.isEmpty){ @if(account.isEmpty){
<fieldset> <fieldset>
<label for="userName"><strong>User name</strong></label> <label for="userName" class="strong">Username:</label>
<input type="text" name="userName" id="userName" value=""/> <input type="text" name="userName" id="userName" value=""/>
<span id="error-userName" class="error"></span> <span id="error-userName" class="error"></span>
</fieldset> </fieldset>
} }
@if(account.map(_.password.nonEmpty).getOrElse(true)){ @if(account.map(_.password.nonEmpty).getOrElse(true)){
<fieldset> <fieldset>
<label for="password"><strong>Password</strong> <label for="password" class="strong">
Password
@if(account.nonEmpty){ @if(account.nonEmpty){
(Input to change password) (input to change password)
} }
:
</label> </label>
<input type="password" name="password" id="password" value=""/> <input type="password" name="password" id="password" value=""/>
<span id="error-password" class="error"></span> <span id="error-password" class="error"></span>
</fieldset> </fieldset>
} }
<fieldset> <fieldset>
<label for="mailAddress"><strong>Mail Address</strong></label> <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)"/> <input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
<span id="error-mailAddress" class="error"></span> <span id="error-mailAddress" class="error"></span>
</fieldset> </fieldset>
<fieldset> <fieldset>
<label for="url"><strong>URL (Optional)</strong></label> <label for="url" class="strong">URL (optional):</label>
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/> <input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
<span id="error-url" class="error"></span> <span id="error-url" class="error"></span>
</fieldset> </fieldset>
</div> </div>
<div class="span6"> <div class="span6">
<fieldset> <fieldset>
<label for="avatar"><strong>Image (Optional)</strong></label> <label for="avatar" class="strong">Image (optional):</label>
@helper.html.uploadavatar(account) @helper.html.uploadavatar(account)
</fieldset> </fieldset>
</div> </div>
</div> </div>
<fieldset class="margin"> <fieldset class="margin">
@if(account.isDefined){ @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"/> <input type="submit" class="btn btn-success" value="Save"/>
<a href="@url(account.get.userName)" class="btn">Cancel</a> <a href="@url(account.get.userName)" class="btn">Cancel</a>
} else { } else {
@@ -57,3 +67,10 @@
</fieldset> </fieldset>
</form> </form>
} }
<script>
$(function(){
$('#delete').click(function(){
return confirm('Once you delete your account, there is no going back.\nAre you sure?');
});
});
</script>

View File

@@ -7,7 +7,8 @@
<div class="span4"> <div class="span4">
<div class="block"> <div class="block">
<div class="account-image">@avatar(account.userName, 200)</div> <div class="account-image">@avatar(account.userName, 200)</div>
<div class="block-header">@account.userName</div> <div class="account-fullname">@account.fullName</div>
<div class="account-username">@account.userName</div>
</div> </div>
<div class="block"> <div class="block">
@if(account.url.isDefined){ @if(account.url.isDefined){

View File

@@ -1,5 +1,6 @@
@(settings: service.SystemSettingsService.SystemSettings, info: Option[Any])(implicit context: app.Context) @(settings: service.SystemSettingsService.SystemSettings, info: Option[Any])(implicit context: app.Context)
@import context._ @import context._
@import util.Directory._
@import view.helpers._ @import view.helpers._
@html.main("System Settings"){ @html.main("System Settings"){
@menu("system"){ @menu("system"){
@@ -8,38 +9,59 @@
<div class="box"> <div class="box">
<div class="box-header">System Settings</div> <div class="box-header">System Settings</div>
<div class="box-content"> <div class="box-content">
<!--====================================================================-->
<!-- GITBUCKET_HOME -->
<!--====================================================================-->
<label class="strong">GITBUCKET_HOME</label>
@GitBucketHome
<!--====================================================================-->
<!-- Base URL -->
<!--====================================================================-->
<hr>
<label><span class="strong">Base URL</span> (e.g. <code>http://example.com/gitbucket</code>)</label>
<fieldset>
<div class="controls">
<input type="text" name="baseUrl" id="baseUrl" style="width: 400px" value="@settings.baseUrl"/>
</div>
</fieldset>
<p>
The base URL is used for redirect, notification email, git repository URL box and more.
If the base URL is empty, GitBucket generates URL from request information.
You can use this property to adjust URL difference between the reverse proxy and GitBucket.
</p>
<!--====================================================================--> <!--====================================================================-->
<!-- Account registration --> <!-- Account registration -->
<!--====================================================================--> <!--====================================================================-->
<label><strong>Account registration</strong></label> <hr>
<label class="strong">Account registration</label>
<fieldset> <fieldset>
<label> <label class="radio">
<input type="radio" name="allowAccountRegistration" value="true"@if(settings.allowAccountRegistration){ checked}> <input type="radio" name="allowAccountRegistration" value="true"@if(settings.allowAccountRegistration){ checked}>
<strong>Allow</strong> - Users can create account by themselves. <span class="strong">Allow</span> - Users can create accounts by themselves.
</label> </label>
<label> <label class="radio">
<input type="radio" name="allowAccountRegistration" value="false"@if(!settings.allowAccountRegistration){ checked}> <input type="radio" name="allowAccountRegistration" value="false"@if(!settings.allowAccountRegistration){ checked}>
<strong>Deny</strong> - Only administrators can create account. <span class="strong">Deny</span> - Only administrators can create accounts.
</label> </label>
</fieldset> </fieldset>
<!--====================================================================--> <!--====================================================================-->
<!-- Services --> <!-- Services -->
<!--====================================================================--> <!--====================================================================-->
<hr> <hr>
<label><strong>Services</strong></label> <label class="strong">Services</label>
<fieldset> <fieldset>
<label> <label class="checkbox">
<input type="checkbox" name="gravatar"@if(settings.gravatar){ checked}/> <input type="checkbox" name="gravatar"@if(settings.gravatar){ checked}/>
Gravatar Use Gravatar for Profile-Images
</label> </label>
</fieldset> </fieldset>
<!--====================================================================--> <!--====================================================================-->
<!-- Authentication --> <!-- Authentication -->
<!--====================================================================--> <!--====================================================================-->
<hr> <hr>
<label><strong>Authentication</strong></label> <label class="strong">Authentication</label>
<fieldset> <fieldset>
<label> <label class="checkbox">
<input type="checkbox" id="ldapAuthentication" name="ldapAuthentication"@if(settings.ldap){ checked}/> <input type="checkbox" id="ldapAuthentication" name="ldapAuthentication"@if(settings.ldap){ checked}/>
LDAP LDAP
</label> </label>
@@ -87,6 +109,13 @@
<span id="error-ldap_userNameAttribute" class="error"></span> <span id="error-ldap_userNameAttribute" class="error"></span>
</div> </div>
</div> </div>
<div class="control-group">
<label class="control-label" for="ldapFullNameAttribute">Full name attribute</label>
<div class="controls">
<input type="text" id="ldapFullNameAttribute" name="ldap.fullNameAttribute" value="@settings.ldap.map(_.fullNameAttribute)"/>
<span id="error-ldap_fullNameAttribute" class="error"></span>
</div>
</div>
<div class="control-group"> <div class="control-group">
<label class="control-label" for="ldapMailAttribute">Mail address attribute</label> <label class="control-label" for="ldapMailAttribute">Mail address attribute</label>
<div class="controls"> <div class="controls">
@@ -94,14 +123,28 @@
<span id="error-ldap_mailAttribute" class="error"></span> <span id="error-ldap_mailAttribute" class="error"></span>
</div> </div>
</div> </div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" name="ldap.tls"@if(settings.ldap.flatMap(_.tls).getOrElse(false)){ checked}/> Enable TLS
</label>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapBindDN">Keystore</label>
<div class="controls">
<input type="text" id="ldapKeystore" name="ldap.keystore" value="@settings.ldap.map(_.keystore)"/>
<span id="error-ldap_keystore" class="error"></span>
</div>
</div>
</div> </div>
<!--====================================================================--> <!--====================================================================-->
<!-- Notification email --> <!-- Notification email -->
<!--====================================================================--> <!--====================================================================-->
<hr> <hr>
<label><strong>Notification email</strong></label> <label class="strong">Notification email</label>
<fieldset> <fieldset>
<label> <label class="checkbox">
<input type="checkbox" id="notification" name="notification"@if(settings.notification){ checked}/> <input type="checkbox" id="notification" name="notification"@if(settings.notification){ checked}/>
Send notifications Send notifications
</label> </label>
@@ -140,6 +183,18 @@
</label> </label>
</div> </div>
</div> </div>
<div class="control-group">
<label class="control-label" for="fromAddress">FROM Address</label>
<div class="controls">
<input type="text" id="fromAddress" name="smtp.fromAddress" value="@settings.smtp.map(_.fromAddress)"/>
</div>
</div>
<div class="control-group">
<label class="control-label" for="fromName">FROM Name</label>
<div class="controls">
<input type="text" id="fromName" name="smtp.fromName" value="@settings.smtp.map(_.fromName)"/>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

@@ -1,16 +1,20 @@
@(users: List[model.Account], members: Map[String, List[String]])(implicit context: app.Context) @(users: List[model.Account], members: Map[String, List[String]], includeRemoved: Boolean)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main("Manage Users"){ @html.main("Manage Users"){
@admin.html.menu("users"){ @admin.html.menu("users"){
<div style="text-align: right; margin-bottom: 4px;"> <div class="pull-right" style="margin-bottom: 4px;">
<a href="@path/admin/users/_newuser" class="btn">New User</a> <a href="@path/admin/users/_newuser" class="btn">New User</a>
<a href="@path/admin/users/_newgroup" class="btn">New Group</a> <a href="@path/admin/users/_newgroup" class="btn">New Group</a>
</div> </div>
<label for="includeRemoved">
<input type="checkbox" id="includeRemoved" name="includeRemoved" @if(includeRemoved){checked}/>
Include removed users
</label>
<table class="table table-bordered table-hover"> <table class="table table-bordered table-hover">
@users.map { account => @users.map { account =>
<tr> <tr>
<td> <td @if(account.isRemoved){style="background-color: #dddddd;"}>
<div class="pull-right"> <div class="pull-right">
@if(account.isGroupAccount){ @if(account.isGroupAccount){
<a href="@path/admin/users/@account.userName/_editgroup">Edit</a> <a href="@path/admin/users/@account.userName/_editgroup">Edit</a>
@@ -57,4 +61,11 @@
} }
</table> </table>
} }
} }
<script>
$(function(){
$('#includeRemoved').click(function(){
location.href = '@path/admin/users?includeRemoved=' + this.checked;
});
});
</script>

View File

@@ -6,45 +6,67 @@
<div class="row-fluid"> <div class="row-fluid">
<div class="span6"> <div class="span6">
<fieldset> <fieldset>
<label for="userName"><strong>Username</strong></label> <label for="userName" class="strong">Username:</label>
<span id="error-userName" class="error"></span> <div>
<span id="error-userName" class="error"></span>
</div>
<input type="text" name="userName" id="userName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/> <input type="text" name="userName" id="userName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
@if(account.isDefined){
<label for="removed">
<input type="checkbox" name="removed" id="removed" value="true" @if(account.get.isRemoved){checked}/>
Disable
</label>
}
</fieldset> </fieldset>
@if(account.map(_.password.nonEmpty).getOrElse(true)){ @if(account.map(_.password.nonEmpty).getOrElse(true)){
<fieldset> <fieldset>
<label for="password"> <label for="password" class="strong">
<strong>Password</strong> Password
@if(account.isDefined){ @if(account.isDefined){
(Input to change password) (Input to change password)
} }
:
</label> </label>
<span id="error-password" class="error"></span> <div>
<span id="error-password" class="error"></span>
</div>
<input type="password" name="password" id="password" value="" autocomplete="off"/> <input type="password" name="password" id="password" value="" autocomplete="off"/>
</fieldset> </fieldset>
} }
<fieldset> <fieldset>
<label for="mailAddress"><strong>Mail Address</strong></label> <label for="fullName" class="strong">Full Name:</label>
<span id="error-mailAddress" class="error"></span> <div>
<span id="error-fullName" class="error"></span>
</div>
<input type="text" name="fullName" id="fullName" value="@account.map(_.fullName)"/>
</fieldset>
<fieldset>
<label for="mailAddress" class="strong">Mail Address:</label>
<div>
<span id="error-mailAddress" class="error"></span>
</div>
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/> <input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
</fieldset> </fieldset>
<fieldset> <fieldset>
<label><strong>User Type</strong></label> <label class="strong">User Type:</label>
<label for="userType_Normal"> <label class="radio" for="userType_Normal">
<input type="radio" name="isAdmin" id="userType_Normal" value="false"@if(account.isEmpty || !account.get.isAdmin){ checked}/> Normal <input type="radio" name="isAdmin" id="userType_Normal" value="false"@if(account.isEmpty || !account.get.isAdmin){ checked}/> Normal
</label> </label>
<label for="userType_Admin"> <label class="radio" for="userType_Admin">
<input type="radio" name="isAdmin" id="userType_Admin" value="true"@if(account.isDefined && account.get.isAdmin){ checked}/> Administrator <input type="radio" name="isAdmin" id="userType_Admin" value="true"@if(account.isDefined && account.get.isAdmin){ checked}/> Administrator
</label> </label>
</fieldset> </fieldset>
<fieldset> <fieldset>
<label><strong>URL (Optional)</strong></label> <label class="strong">URL (Optional):</label>
<span id="error-url" class="error"></span> <div>
<span id="error-url" class="error"></span>
</div>
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/> <input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
</fieldset> </fieldset>
</div> </div>
<div class="span6"> <div class="span6">
<fieldset> <fieldset>
<label for="avatar"><strong>Image (Optional)</strong></label> <label for="avatar" class="strong">Image (Optional)</label>
@helper.html.uploadavatar(account) @helper.html.uploadavatar(account)
</fieldset> </fieldset>
</div> </div>
@@ -55,4 +77,4 @@
</fieldset> </fieldset>
</form> </form>
} }
} }

View File

@@ -1,17 +1,27 @@
@(active: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) @(active: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
<div class="pull-right">
<div class="input-prepend"> @if(repository.commitCount > 0){
<a href="@path/@repository.owner/@repository.name/fork" class="btn" style="margin-bottom: 10px;">Fork</a> <div class="pull-right">
<span class="add-on"><a href="@url(repository)/network/members">@repository.forkedCount</a></span> <div class="input-prepend">
<a href="@path/@repository.owner/@repository.name/fork" class="btn" style="margin-bottom: 10px;">Fork</a>
<span class="add-on"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
</div>
</div> </div>
</div> }
<div class="head"> <div class="head">
<a href="@url(repository.owner)">@repository.owner</a> / <a href="@url(repository)" class="strong">@repository.name</a>
@if(repository.repository.isPrivate){ @if(repository.repository.isPrivate){
<i class="icon-lock"></i> <img src="@assets/common/images/repo_private_lg.png"/>
} else {
@if(repository.repository.originUserName.isDefined){
<img src="@assets/common/images/repo_fork_lg.png"/>
} else {
<img src="@assets/common/images/repo_public_lg.png"/>
}
} }
<a href="@url(repository.owner)">@repository.owner</a> / <a href="@url(repository)" class="strong">@repository.name</a>
@defining(repository.repository){ x => @defining(repository.repository){ x =>
@if(repository.repository.originRepositoryName.isDefined){ @if(repository.repository.originRepositoryName.isDefined){
<div class="forked"> <div class="forked">
@@ -48,10 +58,8 @@
<a href="@url(repository)/settings">Settings</a> <a href="@url(repository)/settings">Settings</a>
</th> </th>
} }
</tr> </tr>
</table> </table>
<form method="POST" id="repository_form">
</form>
<script type="text/javascript"> <script type="text/javascript">
$(function(){ $(function(){
$('table.global-nav th.box-header').click(function(){ $('table.global-nav th.box-header').click(function(){

View File

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

View File

@@ -16,7 +16,9 @@
case "merge_pullreq" => detailActivity(activity, "activity-merge.png") case "merge_pullreq" => detailActivity(activity, "activity-merge.png")
case "create_repository" => simpleActivity(activity, "activity-create-repository.png") case "create_repository" => simpleActivity(activity, "activity-create-repository.png")
case "create_branch" => simpleActivity(activity, "activity-branch.png") case "create_branch" => simpleActivity(activity, "activity-branch.png")
case "delete_branch" => simpleActivity(activity, "activity-delete.png")
case "create_tag" => simpleActivity(activity, "activity-tag.png") case "create_tag" => simpleActivity(activity, "activity-tag.png")
case "delete_tag" => simpleActivity(activity, "activity-delete.png")
case "fork" => simpleActivity(activity, "activity-fork.png") case "fork" => simpleActivity(activity, "activity-fork.png")
case "push" => customActivity(activity, "activity-commit.png"){ case "push" => customActivity(activity, "activity-commit.png"){
<div class="small activity-message"> <div class="small activity-message">
@@ -24,10 +26,12 @@
if(i == 3){ if(i == 3){
<div>...</div> <div>...</div>
} else { } else {
<div> if(commit.nonEmpty){
<a href={s"${path}/${activity.userName}/${activity.repositoryName}/commit/${commit.substring(0, 40)}"} class="monospace">{commit.substring(0, 7)}</a> <div>
<span>{commit.substring(41)}</span> <a href={s"${path}/${activity.userName}/${activity.repositoryName}/commit/${commit. substring(0, 40)}"} class="monospace">{commit.substring(0, 7)}</a>
</div> <span>{commit.substring(41)}</span>
</div>
}
} }
}} }}
</div> </div>

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean, @(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean,
style: String = "", placeholder: String = "Leave a comment")(implicit context: app.Context) style: String = "", placeholder: String = "Leave a comment", elastic: Boolean = false)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
<div class="tabbable"> <div class="tabbable">
@@ -27,6 +27,10 @@
<script src="@assets/google-code-prettify/prettify.js"></script> <script src="@assets/google-code-prettify/prettify.js"></script>
<script> <script>
$(function(){ $(function(){
@if(elastic){
$('#content').elastic();
}
$('#preview').click(function(){ $('#preview').click(function(){
$('#preview-area').html('<img src="@assets/common/images/indicator.gif"> Previewing...'); $('#preview-area').html('<img src="@assets/common/images/indicator.gif"> Previewing...');
$.post('@url(repository)/_preview', { $.post('@url(repository)/_preview', {

View File

@@ -15,6 +15,7 @@
</label> </label>
} }
<input type="hidden" name="fileId" value=""/> <input type="hidden" name="fileId" value=""/>
@if(account.isEmpty || account.get.image.isEmpty){
<script> <script>
$(function(){ $(function(){
var dropzone = new Dropzone('div#clickable', { var dropzone = new Dropzone('div#clickable', {
@@ -32,6 +33,7 @@ $(function(){
}); });
}); });
</script> </script>
}
<style type="text/css"> <style type="text/css">
div.dz-filename, div.dz-size, div.dz-progress, div.dz-success-mark, div.dz-error-mark, div.dz-error-message { div.dz-filename, div.dz-size, div.dz-progress, div.dz-success-mark, div.dz-error-mark, div.dz-error-message {
display: none; display: none;

View File

@@ -31,10 +31,19 @@
@userRepositories.map { repository => @userRepositories.map { repository =>
<tr> <tr>
<td> <td>
@if(repository.owner == loginAccount.get.userName){ @if(repository.repository.isPrivate){
<a href="@url(repository)"><strong>@repository.name</strong></a> <img src="@assets/common/images/repo_private.png"/>
} else { } else {
<a href="@url(repository)">@repository.owner/<strong>@repository.name</strong></a> @if(repository.repository.originUserName.isDefined){
<img src="@assets/common/images/repo_fork.png"/>
} else {
<img src="@assets/common/images/repo_public.png"/>
}
}
@if(repository.owner == loginAccount.get.userName){
<a href="@url(repository)"><span class="strong">@repository.name</span></a>
} else {
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
} }
</td> </td>
</tr> </tr>
@@ -57,7 +66,16 @@
@recentRepositories.map { repository => @recentRepositories.map { repository =>
<tr> <tr>
<td> <td>
<a href="@url(repository)">@repository.owner/<strong>@repository.name</strong></a> @if(repository.repository.isPrivate){
<img src="@assets/common/images/repo_private.png"/>
} else {
@if(repository.repository.originUserName.isDefined){
<img src="@assets/common/images/repo_fork.png"/>
} else {
<img src="@assets/common/images/repo_public.png"/>
}
}
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
</td> </td>
</tr> </tr>
} }

View File

@@ -8,7 +8,7 @@
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div> <div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
<div class="box issue-comment-box"> <div class="box issue-comment-box">
<div class="box-content"> <div class="box-content">
@helper.html.preview(repository, "", false, true, "width: 680px; height: 100px;") @helper.html.preview(repository, "", false, true, "width: 680px; height: 100px; max-height: 150px;", elastic = true)
</div> </div>
</div> </div>
<div class="pull-right"> <div class="pull-right">

View File

@@ -6,21 +6,41 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@comments.map { comment => @comments.map { comment =>
@if(comment.action != "close" && comment.action != "reopen"){ @if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div> <div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
<div class="box issue-comment-box" id="comment-@comment.commentId"> <div class="box issue-comment-box" id="comment-@comment.commentId">
<div class="box-header-small"> <div class="box-header-small">
<i class="icon-comment"></i> <i class="icon-comment"></i>
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> commented @user(comment.commentedUserName, styleClass="username strong")
@if(comment.action == "comment"){
commented
} else {
@if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request }
}
<span class="pull-right"> <span class="pull-right">
@datetime(comment.registeredDate) @datetime(comment.registeredDate)
@if(comment.action != "commit" && comment.action != "merge" && (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){ @if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" &&
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a> (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>&nbsp;
<a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle"></i></a>
} }
</span> </span>
</div> </div>
<div class="box-content"class="issue-content" id="commentContent-@comment.commentId"> <div class="box-content"class="issue-content" id="commentContent-@comment.commentId">
@markdown(comment.content, repository, false, true) @if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){
@defining(comment.content.substring(comment.content.length - 40)){ id =>
<div class="pull-right"><a href="@path/@repository.owner/@repository.name/commit/@id" class="monospace">@id.substring(0, 7)</a></div>
@markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true)
}
} else {
@if(comment.action == "refer"){
@defining(comment.content.split(":")){ case Array(issueId, rest @ _*) =>
<strong><a href="@path/@repository.owner/@repository.name/issues/@issueId">Issue #@issueId</a>: @rest.mkString(":")</strong>
}
} else {
@markdown(comment.content, repository, false, true)
}
}
</div> </div>
</div> </div>
} }
@@ -28,11 +48,11 @@
<div class="small" style="margin-top: 10px; margin-bottom: 10px;"> <div class="small" style="margin-top: 10px; margin-bottom: 10px;">
<span class="label label-info">Merged</span> <span class="label label-info">Merged</span>
@avatar(comment.commentedUserName, 20) @avatar(comment.commentedUserName, 20)
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> merged commit <code>@pullreq.map(_.commitIdTo.substring(0, 7))</code> @user(comment.commentedUserName, styleClass="username strong") merged commit <code>@pullreq.map(_.commitIdTo.substring(0, 7))</code> into
@if(pullreq.get.requestUserName == repository.owner){ @if(pullreq.get.requestUserName == repository.owner){
<span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> to <span class="label label-info monospace">@pullreq.map(_.branch)</span> <span class="label label-info monospace">@pullreq.map(_.branch)</span> from <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span>
} else { } else {
<span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span> to <span class="label label-info monospace">@pullreq.map(_.userName):@pullreq.map(_.branch)</span> <span class="label label-info monospace">@pullreq.map(_.userName):@pullreq.map(_.branch)</span> to <span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span>
} }
@datetime(comment.registeredDate) @datetime(comment.registeredDate)
</div> </div>
@@ -42,9 +62,9 @@
<span class="label label-important">Closed</span> <span class="label label-important">Closed</span>
@avatar(comment.commentedUserName, 20) @avatar(comment.commentedUserName, 20)
@if(issue.isPullRequest){ @if(issue.isPullRequest){
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> closed the pull request @datetime(comment.registeredDate) @user(comment.commentedUserName, styleClass="username strong") closed the pull request @datetime(comment.registeredDate)
} else { } else {
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> closed the issue @datetime(comment.registeredDate) @user(comment.commentedUserName, styleClass="username strong") closed the issue @datetime(comment.registeredDate)
} }
</div> </div>
} }
@@ -52,7 +72,14 @@
<div class="small issue-comment-action"> <div class="small issue-comment-action">
<span class="label label-success">Reopened</span> <span class="label label-success">Reopened</span>
@avatar(comment.commentedUserName, 20) @avatar(comment.commentedUserName, 20)
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> reopened the issue @datetime(comment.registeredDate) @user(comment.commentedUserName, styleClass="username strong") reopened the issue @datetime(comment.registeredDate)
</div>
}
@if(comment.action == "delete_branch"){
<div class="small issue-comment-action">
<span class="label">Deleted</span>
@avatar(comment.commentedUserName, 20)
@user(comment.commentedUserName, styleClass="username strong") deleted the <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> branch @datetime(comment.registeredDate)
</div> </div>
} }
} }
@@ -69,5 +96,18 @@ $(function(){
}); });
return false; return false;
}); });
$('i.icon-remove-circle').click(function(){
if(confirm('Are you sure you want to delete this?')) {
var id = $(this).closest('a').data('comment-id');
$.post('@url(repository)/issue_comments/delete/' + id,
function(data){
if(data > 0) {
$('#comment-' + id).prev('div.issue-avatar-image').remove();
$('#comment-' + id).remove();
}
});
}
return false;
});
}); });
</script> </script>

View File

@@ -56,7 +56,7 @@
</div> </div>
</div> </div>
<hr> <hr>
@helper.html.preview(repository, "", false, true, "width: 600px; height: 200px;") @helper.html.preview(repository, "", false, true, "width: 600px; height: 200px; max-height: 250px;", elastic = true)
</div> </div>
</div> </div>
<div class="pull-right"> <div class="pull-right">
@@ -65,7 +65,7 @@
</div> </div>
<div class="span3"> <div class="span3">
@if(hasWritePermission){ @if(hasWritePermission){
<strong>Add Labels</strong> <span class="strong">Add Labels</span>
<div> <div>
<div id="label-list"> <div id="label-list">
<ul class="label-list nav nav-pills nav-stacked"> <ul class="label-list nav nav-pills nav-stacked">
@@ -97,7 +97,7 @@ $(function(){
} else { } else {
$('#label-assigned').html($('<span>') $('#label-assigned').html($('<span>')
.append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName)) .append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName))
.append(' well be assigned')); .append(' will be assigned'));
$('a.assign[data-name=' + userName + '] i').attr('class', 'icon-ok'); $('a.assign[data-name=' + userName + '] i').attr('class', 'icon-ok');
} }
$('input[name=assignedUserName]').val(userName); $('input[name=assignedUserName]').val(userName);
@@ -111,7 +111,7 @@ $(function(){
if(milestoneId == ''){ if(milestoneId == ''){
$('#label-milestone').text('No milestone'); $('#label-milestone').text('No milestone');
} else { } else {
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<strong>').text(title))); $('#label-milestone').html($('<span>').append('Milestone: ').append($('<span class="strong">').text(title)));
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok'); $('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
} }
$('input[name=milestoneId]').val(milestoneId); $('input[name=milestoneId]').val(milestoneId);

View File

@@ -2,15 +2,17 @@
@import context._ @import context._
<span id="error-edit-content-@commentId" class="error"></span> <span id="error-edit-content-@commentId" class="error"></span>
<textarea style="width: 680px; height: 100px;" id="edit-content-@commentId">@content</textarea> <textarea style="width: 680px; height: 100px;" id="edit-content-@commentId">@content</textarea>
<input type="button" class="btn btn-small" value="Update Comment"/> <div>
<span class="pull-right"><a class="btn btn-small btn-danger" href="#">Cancel</a></span> <input type="button" id="update-comment-@commentId" class="btn btn-small" value="Update Comment"/>
<input type="button" id="cancel-comment-@commentId" class="btn btn-small btn-danger pull-right" value="Cancel"/>
</div>
<script> <script>
$(function(){ $(function(){
var callback = function(data){ var callback = function(data){
$('#commentContent-@commentId').empty().html(data.content); $('#commentContent-@commentId').empty().html(data.content);
}; };
$('#commentContent-@commentId input.btn').click(function(){ $('#update-comment-@commentId').click(function(){
$.ajax({ $.ajax({
url: '@path/@owner/@repository/issue_comments/edit/@commentId', url: '@path/@owner/@repository/issue_comments/edit/@commentId',
type: 'POST', type: 'POST',
@@ -25,7 +27,7 @@ $(function(){
}); });
}); });
$('#commentContent-@commentId a.btn').click(function(){ $('#cancel-comment-@commentId').click(function(){
$.get('@path/@owner/@repository/issue_comments/_data/@commentId', callback); $.get('@path/@owner/@repository/issue_comments/_data/@commentId', callback);
return false; return false;
}); });

View File

@@ -2,17 +2,21 @@
@import context._ @import context._
<span id="error-edit-title" class="error"></span> <span id="error-edit-title" class="error"></span>
<input type="text" style="width: 680px;" id="edit-title" value="@title"/> <input type="text" style="width: 680px;" id="edit-title" value="@title"/>
<textarea style="width: 680px; height: 100px;" id="edit-content">@content.getOrElse("")</textarea> <textarea style="width: 680px; height: 100px; max-height: 300px;" id="edit-content">@content.getOrElse("")</textarea>
<input type="button" class="btn btn-small" value="Update Issue"/> <div>
<span class="pull-right"><a class="btn btn-small btn-danger" href="#">Cancel</a></span> <input type="button" id="update" class="btn btn-small" value="Update Issue"/>
<input type="button" id="cancel" class="btn btn-small btn-danger pull-right" value="Cancel"/>
</div>
<script> <script>
$(function(){ $(function(){
$('#edit-content').elastic();
var callback = function(data){ var callback = function(data){
$('#issueTitle').empty().text(data.title); $('#issueTitle').empty().text(data.title);
$('#issueContent').empty().html(data.content); $('#issueContent').empty().html(data.content);
}; };
$('#issueContent input.btn').click(function(){ $('#update').click(function(){
$.ajax({ $.ajax({
url: '@path/@owner/@repository/issues/edit/@issueId', url: '@path/@owner/@repository/issues/edit/@issueId',
type: 'POST', type: 'POST',
@@ -27,7 +31,7 @@ $(function(){
}); });
}); });
$('#issueContent a.btn').click(function(){ $('#cancel').click(function(){
$.get('@path/@owner/@repository/issues/_data/@issueId', callback); $.get('@path/@owner/@repository/issues/_data/@issueId', callback);
return false; return false;
}); });

View File

@@ -29,55 +29,11 @@
} }
<div class="small" style="text-align: center;"> <div class="small" style="text-align: center;">
@defining(comments.filter( _.action.contains("comment") ).size){ count => @defining(comments.filter( _.action.contains("comment") ).size){ count =>
<strong>@count</strong> @plural(count, "comment") <span class="strong">@count</span> @plural(count, "comment")
} }
</div> </div>
<hr/> <hr/>
<div style="margin-bottom: 8px;"> @issues.html.labels(issue, issueLabels, labels, hasWritePermission, repository)
<strong>Labels</strong>
@if(hasWritePermission){
<div class="pull-right">
@helper.html.dropdown() {
@labels.map { label =>
<li>
<a href="#" class="toggle-label" data-label-id="@label.labelId">
<i class="@{if(issueLabels.exists(_.labelId == label.labelId)) "icon-ok" else "icon-white"}"></i>
<span class="label" style="background-color: #@label.color;">&nbsp;</span>
@label.labelName
</a>
</li>
}
}
</div>
}
</div>
<ul class="label-list nav nav-pills nav-stacked">
@labellist(issueLabels)
</ul>
</div> </div>
</div> </div>
} }
<script>
$(function(){
$('a.toggle-label').click(function(){
var path, icon;
var i = $(this).children('i');
if(i.hasClass('icon-ok')){
path = 'delete';
icon = 'icon-white';
} else {
path = 'new';
icon = 'icon-ok';
}
$.post('@url(repository)/issues/@issue.issueId/label/' + path,
{
labelId : $(this).data('label-id')
},
function(data){
i.removeClass().addClass(icon);
$('ul.label-list').empty().html(data);
});
return false;
});
});
</script>

View File

@@ -14,21 +14,25 @@
<span class="pull-right"><a class="btn btn-small" href="#" id="edit">Edit</a></span> <span class="pull-right"><a class="btn btn-small" href="#" id="edit">Edit</a></span>
} }
<div class="small muted"> <div class="small muted">
<a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a> opened this issue @datetime(issue.registeredDate) @user(issue.openedUserName, styleClass="username strong") opened this issue @datetime(issue.registeredDate)
</div> </div>
<h4 id="issueTitle">@issue.title</h4> <h4 id="issueTitle">@issue.title</h4>
</div> </div>
<div class="issue-info"> <div class="issue-info">
<span id="label-assigned"> <span id="label-assigned">
@issue.assignedUserName.map { userName => @issue.assignedUserName.map { userName =>
@avatar(userName, 20) <a href="@url(userName)" class="username strong">@userName</a> is assigned @avatar(userName, 20) @user(userName, styleClass="username strong") is assigned
}.getOrElse("No one is assigned") }.getOrElse("No one is assigned")
</span> </span>
@if(hasWritePermission){ @if(hasWritePermission){
@helper.html.dropdown() { @helper.html.dropdown() {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li> <li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
@collaborators.map { collaborator => @collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li> <li>
<a href="javascript:void(0);" class="assign" data-name="@collaborator">
@helper.html.checkicon(Some(collaborator) == issue.assignedUserName)@avatar(collaborator, 20) @collaborator
</a>
</li>
} }
} }
} }
@@ -36,7 +40,7 @@
<span id="label-milestone"> <span id="label-milestone">
@issue.milestoneId.map { milestoneId => @issue.milestoneId.map { milestoneId =>
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) => @milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
Milestone: <strong>@milestone.title</strong> Milestone: <span class="strong">@milestone.title</span>
} }
}.getOrElse("No milestone") }.getOrElse("No milestone")
</span> </span>
@@ -53,7 +57,7 @@
@milestones.map { case (milestone, _, _) => @milestones.map { case (milestone, _, _) =>
<li> <li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title"> <a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
<i class="icon-white"></i> @milestone.title @helper.html.checkicon(Some(milestone.milestoneId) == issue.milestoneId) @milestone.title
<div class="small" style="padding-left: 20px;"> <div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate => @milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){ @if(isPast(dueDate)){
@@ -79,19 +83,12 @@
</div> </div>
<div class="issue-participants"> <div class="issue-participants">
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants => @defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
<strong>@participants.size</strong> @plural(participants.size, "participant") <span class="strong">@participants.size</span> @plural(participants.size, "participant")
@participants.map { participant => <a href="@url(participant)">@avatar(participant, 20, tooltip = true)</a> } @participants.map { participant => @avatarLink(participant, 20, tooltip = true) }
} }
</div> </div>
<script> <script>
$(function(){ $(function(){
@if(issue.assignedUserName.isDefined){
$('a.assign[data-name=@issue.assignedUserName] i').attr('class', 'icon-ok');
}
@if(issue.milestoneId.isDefined){
$('a.milestone[data-id=@issue.milestoneId] i').attr('class', 'icon-ok');
}
$('#edit').click(function(){ $('#edit').click(function(){
$.get('@url(repository)/issues/_data/@issue.issueId', $.get('@url(repository)/issues/_data/@issue.issueId',
{ {
@@ -138,7 +135,7 @@ $(function(){
$('#label-milestone').text('No milestone'); $('#label-milestone').text('No milestone');
$('#milestone-progress-area').empty(); $('#milestone-progress-area').empty();
} else { } else {
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<strong>').text(title))); $('#label-milestone').html($('<span>').append('Milestone: ').append($('<span class="strong">').text(title)));
$('#milestone-progress-area').html(data); $('#milestone-progress-area').html(data);
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok'); $('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
} }

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