Compare commits

..

114 Commits

Author SHA1 Message Date
takezoe
5317ac5e03 Update README.md 2014-03-29 16:24:06 +09:00
Naoki Takezoe
9df1467ddf Merge pull request #329 from andytait/master
README typo fix.
2014-03-28 14:13:15 +09:00
Andy Tait
df79bd4515 README typo fix. 2014-03-27 10:11:56 +00:00
Naoki Takezoe
cbb14f2ba8 Update README.md 2014-03-25 05:20:03 +09:00
takezoe
1fe649e70f (refs #326)SSH clone URL includes username for logged-in users and it's hidden for non logged-in users. 2014-03-25 05:11:29 +09:00
Naoki Takezoe
0d918add28 Merge pull request #320 from kunigaku/fix_247
Fix #247 servlet.CommitLogHook.onPostReceive takes minutes
2014-03-25 04:51:04 +09:00
Naoki Takezoe
3926c98338 Merge pull request #286 from maliayas/patch-2
Improve code view
2014-03-25 02:06:20 +09:00
takezoe
ec0c964ceb Fix #325 2014-03-20 17:50:25 +09:00
Naoki Takezoe
b4fd90c6d3 Merge pull request #323 from r0n22/patch-1
Documentation Change for Large file Sizes
2014-03-20 10:59:11 +09:00
r0n22
7dfd63cfa2 Update README.md
Ran into this issue today.  Added to documentation to let other users know about it.
2014-03-19 14:21:30 -04:00
Naoki Takezoe
a562e5ca14 Merge pull request #319 from jmu/master
Trim LF of version file
2014-03-19 04:04:31 +09:00
Naoki Takezoe
2885eef4ab Merge pull request #321 from xuwei-k/sbt0.13
sbt 0.13
2014-03-19 04:03:15 +09:00
xuwei-k
087297d14c sbt 0.13 2014-03-19 00:48:57 +09:00
kunigaku
6e0fb95ac3 Fix #247 servlet.CommitLogHook.onPostReceive takes minutes
When pushing tags, command.getOldId value is
“0000000000000000000000000000000000000000”.
So, can not get commit logs.
2014-03-18 22:51:15 +09:00
takezoe
61e28146fb Fix TODO 2014-03-16 02:10:48 +09:00
takezoe
40d3f0ef9e (refs #313)Search files which have FileMode.EXECUTABLE_FILE mode not only REGULAR_FILE 2014-03-16 01:55:26 +09:00
takezoe
99db825114 (refs #313)Add files which have FileMode.EXECUTABLE_FILE mode into zip file 2014-03-16 01:53:14 +09:00
takezoe
7341b377fe Fix #314 2014-03-15 23:10:21 +09:00
takezoe
7f78a98de0 Display the first line of commit message on the file list 2014-03-15 18:03:52 +09:00
takezoe
a64207f0ec Focus error field after validation. 2014-03-15 16:37:43 +09:00
takezoe
d86f40e3a2 Small fix 2014-03-15 16:06:49 +09:00
Jiang
b74417f393 trim LF of version file 2014-03-15 10:14:41 +08:00
takezoe
f5883abf04 Use Context#settings instead of loadSystemSettings() 2014-03-15 04:07:31 +09:00
takezoe
02a367fd99 Add SystemSettings to Context properties 2014-03-15 01:24:34 +09:00
takezoe
4870533710 Upgrade scalatra-forms to 0.0.14 2014-03-15 01:04:28 +09:00
takezoe
8170a1b01d (refs #115)Display finger print on the ssh key setting page 2014-03-14 11:39:40 +09:00
takezoe
d1c6c763e2 (refs #115)Add button to toggle http/ssh for wiki repository url box 2014-03-14 10:59:42 +09:00
takezoe
0c683f7243 (refs #115)SSH access to Wiki repository is available 2014-03-14 10:44:38 +09:00
takezoe
63de780527 Remove last '/' in base url 2014-03-14 03:21:35 +09:00
takezoe
c5ccbf2d1f (refs #115)Fix TestCase 2014-03-14 02:57:31 +09:00
takezoe
8777535431 (refs #115)Base URL is required if SSH access is enabled 2014-03-14 02:07:31 +09:00
takezoe
70192ce420 (refs #115)Add new method to get sshUrl to RepositoryInfo 2014-03-13 22:26:15 +09:00
takezoe
02d79cb16a (refs #115)Add url switcher to the repository url box 2014-03-13 16:54:11 +09:00
Tomofumi Tanaka
78ca9b3f1a (refs #115)Use system settings port number 2014-03-13 01:47:28 +09:00
Tomofumi Tanaka
017631e337 (refs #115)Add ShellFactory 2014-03-13 00:56:33 +09:00
Tomofumi Tanaka
f9078dff2c (refs #115)Remove debug code 2014-03-13 00:45:29 +09:00
takezoe
b66381d677 Merge branch 'master' into ssh-access
Conflicts:
	src/main/scala/servlet/GitRepositoryServlet.scala
2014-03-12 22:43:32 +09:00
takezoe
49bf88f7a7 (refs #312)Fix redirection for non git client 2014-03-12 22:39:12 +09:00
Tomofumi Tanaka
f93ceaa91d (refs #115)Fix typo 2014-03-12 01:01:12 +09:00
Tomofumi Tanaka
0fe122dc63 (refs #115)Parse owner and repository correctly 2014-03-12 00:57:03 +09:00
takezoe
3d251fa8ad (refs #115)Disable ssh key setting page if ssh access is disabled 2014-03-11 02:37:02 +09:00
takezoe
af0b52448a (refs #115)Add repository permission checking 2014-03-11 02:30:11 +09:00
takezoe
f78cdb637d (refs #115)Fix TestCase 2014-03-10 01:00:10 +09:00
takezoe
845f2d6faa (refs #115)Start and stop sshd at the system settings 2014-03-10 00:44:25 +09:00
takezoe
525edbab80 (refs #115)Add TODO 2014-03-09 02:27:46 +09:00
takezoe
c422b1c9a5 (refs #115)Small fix for views 2014-03-09 02:12:32 +09:00
takezoe
1043b13228 (refs #115)Small fix for views 2014-03-09 01:48:30 +09:00
takezoe
5e6c33df6c (refs #115)Fix account registration page and add public key deletion 2014-03-09 01:38:23 +09:00
takezoe
9541771703 (refs #115)Use registered public key for authentication 2014-03-09 01:08:04 +09:00
takezoe
f99d37cfad (refs #115)Adding SSH Key form is available 2014-03-09 00:57:00 +09:00
takezoe
0cfe31ccd9 (refs #115)Fix TestCase 2014-03-08 22:00:36 +09:00
takezoe
8fc1a5473b (refs #115)Add table and model to store public keys 2014-03-08 19:26:49 +09:00
takezoe
049b12b908 Merge branch 'master' into ssh-access
Conflicts:
	src/main/scala/servlet/GitRepositoryServlet.scala
2014-03-08 18:59:13 +09:00
takezoe
45f992b2bc (refs #115)ServletContext is passed to Command 2014-03-08 14:57:17 +09:00
takezoe
9e2c66c341 (refs #115)ServletContext is passed to Command 2014-03-08 14:48:58 +09:00
takezoe
2d0f59b6f2 (refs #307)Fix deletion problem for branches which contains '/' 2014-03-08 04:22:55 +09:00
takezoe
fbba29e810 (refs #303)Submodule support 2014-03-08 02:58:49 +09:00
Tomofumi Tanaka
07a108760c (refs #115)Use regex to create git command
And add GitCommandFactory Unit Test Spec
2014-03-06 23:36:01 +09:00
takezoe
b641bfb56a (refs #241)Modify AccountService#getGroupMembers() to returns list of GroupMember instead of Tuple2 2014-03-06 16:17:41 +09:00
takezoe
c65d80bc72 (refs #241)Fix error occurring when member name contains special characters. 2014-03-06 15:51:12 +09:00
takezoe
e0d266bf16 (refs #241)Allow group manager to use repository settings 2014-03-06 15:30:58 +09:00
takezoe
b62f7c5aee Fix TestCase 2014-03-06 14:27:49 +09:00
takezoe
c89f04b926 (refs #241)Fix group editing in the administration console 2014-03-06 12:17:25 +09:00
takezoe
ff8b4b4a88 Move newrepo.scala.html to account package 2014-03-06 11:39:41 +09:00
takezoe
07d63ae63a Add caret to new icon 2014-03-06 11:39:13 +09:00
takezoe
c0f5cb1641 Move group.scala.html to account package 2014-03-06 11:26:58 +09:00
takezoe
50d84835cb Merge CreateController to AccountController 2014-03-06 11:24:28 +09:00
takezoe
8cdf4ef618 Merge branch 'improve-group' 2014-03-06 11:06:19 +09:00
takezoe
eff3a7acb4 (refs #241)Fix group creation / edition form presentation 2014-03-06 11:05:47 +09:00
takezoe
716eddac7b (refs #241)Checkbox for manager is replaced with toggle buttons 2014-03-06 02:46:40 +09:00
takezoe
9b15af3bb7 Update README.md for 1.11.1 release 2014-03-05 22:16:37 +09:00
takezoe
b732e0d55a Fix error when base url is configured. 2014-03-05 22:12:34 +09:00
takezoe
d92a1cee1c Replace Context#path with the base url if it's configured. 2014-03-05 19:18:50 +09:00
takezoe
c7a2ec8290 (refs #299)Fix redirection path in PullRequestsController 2014-03-05 16:23:27 +09:00
shimamoto
145c155ba5 Remove unnecessary import. 2014-03-05 14:40:52 +09:00
shimamoto
6f9ef32d96 (refs #292) Fix to limit issue result before joins issue labels. 2014-03-05 14:35:49 +09:00
takezoe
aa5b9dbbbd Remove unnecessary code 2014-03-05 04:23:44 +09:00
takezoe
f11be44c02 (refs #330)Return NotFound if specified file does not exist 2014-03-05 04:18:45 +09:00
Tomofumi Tanaka
d46589ad29 (refs #115)Ensure exit status 1 on GitCommand error 2014-03-04 23:19:58 +09:00
Tomofumi Tanaka
09b7e67c52 (refs #110)Correct authentication and CommitHook 2014-03-04 22:49:25 +09:00
Tomofumi Tanaka
79e1abe624 (refs #110)Update apache sshd version
For fix authentication partial bug
2014-03-04 22:42:32 +09:00
takezoe
9bd1f0a492 (refs #241)Remove unnecessary code 2014-03-04 10:53:26 +09:00
takezoe
7a2c82461e (refs #241)Group management improvement is completed in user side 2014-03-04 10:52:37 +09:00
takezoe
21f7888f55 (refs #241)Add validation for create / edit group form. 2014-03-04 10:23:44 +09:00
takezoe
e3fd564efd (refs #241)Work for specifying group manager 2014-03-04 04:25:44 +09:00
takezoe
5cf96134d5 (refs #296)Fix redirection path generation again 2014-03-04 03:25:18 +09:00
takezoe
607c477e7d Add options for remote debugging 2014-03-04 02:39:18 +09:00
takezoe
17920e1195 (refs #198)Allow group editing by group members. 2014-03-03 01:45:00 +09:00
Naoki Takezoe
721454aa90 Merge pull request #294 from eiryu/work
fix typo
2014-03-03 01:21:59 +09:00
takezoe
d870896cfb Rename CreateRepositoryController to CreateController. 2014-03-03 01:21:22 +09:00
takezoe
270eb7cf1d (refs #198)Allow create group by normal users. 2014-03-03 01:17:52 +09:00
eiryu
527fd94145 fix typo 2014-03-03 00:28:48 +09:00
takezoe
04e4572088 Merge branch '1.11-update' 2014-03-03 00:04:18 +09:00
takezoe
ec09adf03e Merge branch 'odz-closing-issues-via-commit-messages' 2014-03-02 04:49:53 +09:00
takezoe
b031103df8 (refs #218)Separate StringUtil#extractCloseId() to add unit test. 2014-03-02 04:49:14 +09:00
takezoe
7701521a2e Fix to use Exception#ignoring to ignore Exception. 2014-03-02 04:24:24 +09:00
takezoe
0c683d4f75 Merge branch 'closing-issues-via-commit-messages' of https://github.com/odz/gitbucket into odz-closing-issues-via-commit-messages 2014-03-02 04:02:45 +09:00
Naoki Takezoe
200d095034 Merge pull request #290 from lefou/compare-email-caseinsensitive
Compare email adresses case-insensitive.
2014-03-02 01:35:00 +09:00
takezoe
94576a876a (refs #280)Commit count limitation is changed to 10000 from 1000. 2014-03-02 01:23:30 +09:00
takezoe
0fa1922bb0 Small fix to pull request #289 2014-03-02 01:05:42 +09:00
Naoki Takezoe
c557905858 Merge pull request #289 from lefou/description-in-header
Show repository description below the name on repository page.
2014-03-02 00:56:07 +09:00
takezoe
31b21d74b1 Remove --https option. 2014-03-02 00:25:56 +09:00
Tomofumi Tanaka
2e236e90ba (refs #110) Add CommitHook sample
WIP: Some important functions are not implement yet.
2014-03-01 02:09:24 +09:00
Tomofumi Tanaka
c5aee0810c (refs #115) Add PublicKeyAuthenticator sample impl
Auth TODOs

* Fetch user account pubkeys from DB
2014-03-01 01:00:00 +09:00
Tobias Roeser
f13d757976 Compare email adresses case-insensitive. 2014-02-28 16:28:40 +01:00
Tobias Roeser
7a0a62af2d Show repository description below the name on repository page. 2014-02-28 15:25:32 +01:00
Tomofumi Tanaka
ceab1d2fd2 (refs #115) Reorganize ssh sources 2014-02-28 22:41:01 +09:00
Tomofumi Tanaka
891ca70ade refs #115: Support git access via ssh (1st step)
EXPERIMENTAL

TODOs

* Authentication (PublicKey)
* Publickey management in profile view
* Commit hooks integration (WebHook too)
* Parse command correctly
* Make Configurable many options(enable/disable, port, etc...)
* ShellProcessFactory
* Test, test, test...
2014-02-27 02:05:58 +09:00
Tomofumi Tanaka
9ed2a50d26 Add SSH Service Listener 2014-02-27 02:02:37 +09:00
Ali Ayas
057c5f073c Improve diff view 2014-02-26 12:09:25 +02:00
Ali Ayas
e902da6595 Improve code view
Now it's more similar to GitHub
2014-02-26 11:40:15 +02:00
odz
8b5414c8f7 Merge branch 'master' into closing-issues-via-commit-messages
Conflicts:
	src/main/scala/app/PullRequestsController.scala
	src/main/scala/servlet/GitRepositoryServlet.scala
2014-02-25 00:07:25 +09:00
Tomofumi Tanaka
c86ece4dc0 Add mina ssh to dependencies 2014-02-24 22:45:45 +09:00
odz
dc78dc9b0d Close issues via commit messages 2013-11-30 18:57:19 +09:00
78 changed files with 1717 additions and 589 deletions

View File

@@ -23,7 +23,7 @@ Following features are not implemented, but we will make them in the future rele
- File editing in repository viewer
- Comment for the changeset
- Network graph
- Statics
- Statistics
- Watch / Star
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki).
@@ -35,6 +35,8 @@ Installation
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.
If you are using Gitbucket behind a webserver please make sure you have increased the **client_max_body_size** (on nignx)
The default administrator account is **root** and password is **root**.
or you can start GitBucket by `java -jar gitbucket.war` without servlet container. In this case, GitBucket URL is **http://[hostname]:8080/**. You can specify following options.
@@ -58,6 +60,16 @@ Run the following commands in `Terminal` to
Release Notes
--------
### 1.12 - 29 Mar 2014
- SSH repository access is available
- Allow users can create and management their groups
- Git submodule support
- Close issues via commit messages
- Show repository description below the name on repository page
- Fix presentation of the source viewer
- Upgrade to sbt 0.13
- Fix some bugs
### 1.11.1 - 06 Mar 2014
- Bug fix

View File

@@ -4,9 +4,6 @@
# Server port
#GITBUCKET_PORT=8080
# Force HTTPS scheme
#GITBUCKET_HTTPS=false
# Data directory (GITBUCKET_HOME/gitbucket)
#GITBUCKET_HOME=/var/lib/gitbucket

View File

@@ -39,9 +39,6 @@ start() {
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 &

View File

@@ -1 +1 @@
sbt.version=0.12.3
sbt.version=0.13.1

View File

@@ -30,12 +30,13 @@ object MyBuild extends Build {
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
"org.json4s" %% "json4s-jackson" % "3.2.5",
"jp.sf.amateras" %% "scalatra-forms" % "0.0.11",
"jp.sf.amateras" %% "scalatra-forms" % "0.0.14",
"commons-io" % "commons-io" % "2.4",
"org.pegdown" % "pegdown" % "1.4.1",
"org.apache.commons" % "commons-compress" % "1.5",
"org.apache.commons" % "commons-email" % "1.3.1",
"org.apache.httpcomponents" % "httpclient" % "4.3",
"org.apache.sshd" % "apache-sshd" % "0.10.0",
"com.typesafe.slick" %% "slick" % "1.0.1",
"com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.3.173",

View File

@@ -1,9 +1,11 @@
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.2.0")
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.4.0")
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.1")
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0")
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.0")
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.5")
addSbtPlugin("io.spray" % "sbt-twirl" % "0.6.1")
resolvers += "spray repo" at "http://repo.spray.io"
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.2")
addSbtPlugin("io.spray" % "sbt-twirl" % "0.7.0")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.4")

Binary file not shown.

BIN
sbt-launch-0.13.1.jar Normal file

Binary file not shown.

View File

@@ -1,2 +1,2 @@
set SCRIPT_DIR=%~dp0
java -Dsbt.log.noformat=true -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 -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.1.jar" %*

2
sbt.sh
View File

@@ -1 +1 @@
java -Dsbt.log.noformat=true -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 -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.1.jar "$@"

View File

@@ -0,0 +1,11 @@
ALTER TABLE GROUP_MEMBER ADD COLUMN MANAGER BOOLEAN DEFAULT FALSE;
CREATE TABLE SSH_KEY (
USER_NAME VARCHAR(100) NOT NULL,
SSH_KEY_ID INT AUTO_INCREMENT,
TITLE VARCHAR(100) NOT NULL,
PUBLIC_KEY TEXT NOT NULL
);
ALTER TABLE SSH_KEY ADD CONSTRAINT IDX_SSH_KEY_PK PRIMARY KEY (USER_NAME, SSH_KEY_ID);
ALTER TABLE SSH_KEY ADD CONSTRAINT IDX_SSH_KEY_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);

View File

@@ -20,7 +20,6 @@ class ScalatraBootstrap extends LifeCycle {
context.mount(new DashboardController, "/*")
context.mount(new UserManagementController, "/*")
context.mount(new SystemSettingsController, "/*")
context.mount(new CreateRepositoryController, "/*")
context.mount(new AccountController, "/*")
context.mount(new RepositoryViewerController, "/*")
context.mount(new WikiController, "/*")

View File

@@ -1,17 +1,25 @@
package app
import service._
import util.{FileUtil, OneselfAuthenticator}
import util._
import util.StringUtil._
import util.Directory._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.{FileMode, Constants}
import org.eclipse.jgit.dircache.DirCache
import model.GroupMember
class AccountController extends AccountControllerBase
with AccountService with RepositoryService with ActivityService with OneselfAuthenticator
with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
trait AccountControllerBase extends AccountManagementControllerBase {
self: AccountService with RepositoryService with ActivityService with OneselfAuthenticator =>
self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator =>
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String])
@@ -19,6 +27,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
case class AccountEditForm(password: Option[String], fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String], clearImage: Boolean)
case class SshKeyForm(title: String, publicKey: String)
val newForm = mapping(
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" , text(required, maxlength(20)))),
@@ -37,6 +47,45 @@ trait AccountControllerBase extends AccountManagementControllerBase {
"clearImage" -> trim(label("Clear image" , boolean()))
)(AccountEditForm.apply)
val sshKeyForm = mapping(
"title" -> trim(label("Title", text(required, maxlength(100)))),
"publicKey" -> trim(label("Key" , text(required)))
)(SshKeyForm.apply)
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String)
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String, clearImage: Boolean)
val newGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members)))
)(NewGroupForm.apply)
val editGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members))),
"clearImage" -> trim(label("Clear image" ,boolean()))
)(EditGroupForm.apply)
case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
case class ForkRepositoryForm(owner: String, name: String)
val newRepositoryForm = mapping(
"owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, uniqueRepository))),
"description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())),
"createReadme" -> trim(label("Create README" , boolean()))
)(RepositoryCreationForm.apply)
val forkRepositoryForm = mapping(
"owner" -> trim(label("Repository owner", text(required))),
"name" -> trim(label("Repository name", text(required)))
)(ForkRepositoryForm.apply)
/**
* Displays user information.
*/
@@ -51,14 +100,20 @@ trait AccountControllerBase extends AccountManagementControllerBase {
getActivitiesByUser(userName, true))
// Members
case "members" if(account.isGroupAccount) =>
_root_.account.html.members(account, getGroupMembers(account.userName))
case "members" if(account.isGroupAccount) => {
val members = getGroupMembers(account.userName)
_root_.account.html.members(account, members.map(_.userName),
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
}
// Repositories
case _ =>
case _ => {
val members = getGroupMembers(account.userName)
_root_.account.html.repositories(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
getVisibleRepositories(context.loginAccount, baseUrl, Some(userName)))
getVisibleRepositories(context.loginAccount, baseUrl, Some(userName)),
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
}
}
} getOrElse NotFound
}
@@ -76,7 +131,9 @@ trait AccountControllerBase extends AccountManagementControllerBase {
get("/:userName/_edit")(oneselfOnly {
val userName = params("userName")
getAccountByUserName(userName).map(x => account.html.edit(Some(x), flash.get("info"))) getOrElse NotFound
getAccountByUserName(userName).map { x =>
account.html.edit(x, flash.get("info"))
} getOrElse NotFound
})
post("/:userName/_edit", editForm)(oneselfOnly { form =>
@@ -116,22 +173,266 @@ trait AccountControllerBase extends AccountManagementControllerBase {
redirect("/")
})
get("/:userName/_ssh")(oneselfOnly {
val userName = params("userName")
getAccountByUserName(userName).map { x =>
account.html.ssh(x, getPublicKeys(x.userName))
} getOrElse NotFound
})
post("/:userName/_ssh", sshKeyForm)(oneselfOnly { form =>
val userName = params("userName")
addPublicKey(userName, form.title, form.publicKey)
redirect(s"/${userName}/_ssh")
})
get("/:userName/_ssh/delete/:id")(oneselfOnly {
val userName = params("userName")
val sshKeyId = params("id").toInt
deletePublicKey(userName, sshKeyId)
redirect(s"/${userName}/_ssh")
})
get("/register"){
if(loadSystemSettings().allowAccountRegistration){
if(context.settings.allowAccountRegistration){
if(context.loginAccount.isDefined){
redirect("/")
} else {
account.html.edit(None, None)
account.html.register()
}
} else NotFound
}
post("/register", newForm){ form =>
if(loadSystemSettings().allowAccountRegistration){
if(context.settings.allowAccountRegistration){
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url)
updateImage(form.userName, form.fileId, false)
redirect("/signin")
} else NotFound
}
get("/groups/new")(usersOnly {
account.html.group(None, List(GroupMember("", context.loginAccount.get.userName, true)))
})
post("/groups/new", newGroupForm)(usersOnly { form =>
createGroup(form.groupName, form.url)
updateGroupMembers(form.groupName, form.members.split(",").map {
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList)
updateImage(form.groupName, form.fileId, false)
redirect(s"/${form.groupName}")
})
get("/:groupName/_editgroup")(managersOnly {
defining(params("groupName")){ groupName =>
account.html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
}
})
get("/:groupName/_deletegroup")(managersOnly {
defining(params("groupName")){ groupName =>
// Remove from GROUP_MEMBER
updateGroupMembers(groupName, Nil)
// Remove repositories
getRepositoryNamesOfUser(groupName).foreach { repositoryName =>
deleteRepository(groupName, repositoryName)
FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
}
}
redirect("/")
})
post("/:groupName/_editgroup", editGroupForm)(managersOnly { form =>
defining(params("groupName"), form.members.split(",").map {
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList){ case (groupName, members) =>
getAccountByUserName(groupName, true).map { account =>
updateGroup(groupName, form.url, false)
// Update GROUP_MEMBER
updateGroupMembers(form.groupName, members)
// Update COLLABORATOR for group repositories
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
removeCollaborators(form.groupName, repositoryName)
members.foreach { case (userName, isManager) =>
addCollaborator(form.groupName, repositoryName, userName)
}
}
updateImage(form.groupName, form.fileId, form.clearImage)
redirect(s"/${form.groupName}")
} getOrElse NotFound
}
})
/**
* Show the new repository form.
*/
get("/new")(usersOnly {
account.html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
})
/**
* Create new repository.
*/
post("/new", newRepositoryForm)(usersOnly { form =>
LockUtil.lock(s"${form.owner}/${form.name}/create"){
if(getRepository(form.owner, form.name, baseUrl).isEmpty){
val ownerAccount = getAccountByUserName(form.owner).get
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
// Insert to the database at first
createRepository(form.name, form.owner, form.description, form.isPrivate)
// Add collaborators for group repository
if(ownerAccount.isGroupAccount){
getGroupMembers(form.owner).foreach { member =>
addCollaborator(form.owner, form.name, member.userName)
}
}
// Insert default labels
insertDefaultLabels(form.owner, form.name)
// Create the actual repository
val gitdir = getRepositoryDir(form.owner, form.name)
JGitUtil.initRepository(gitdir)
if(form.createReadme){
using(Git.open(gitdir)){ git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
val content = if(form.description.nonEmpty){
form.name + "\n" +
"===============\n" +
"\n" +
form.description.get
} else {
form.name + "\n" +
"===============\n"
}
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
}
}
// Create Wiki repository
createWikiRepository(loginAccount, form.owner, form.name)
// Record activity
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
}
// redirect to the repository
redirect(s"/${form.owner}/${form.name}")
}
})
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
LockUtil.lock(s"${loginUserName}/${repository.name}/create"){
if(repository.owner == loginUserName){
// redirect to the repository
redirect(s"/${repository.owner}/${repository.name}")
} else {
getForkedRepositories(repository.owner, repository.name).find(_._1 == loginUserName).map { case (owner, name) =>
// redirect to the repository
redirect(s"/${owner}/${name}")
} getOrElse {
// Insert to the database at first
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
createRepository(
repositoryName = repository.name,
userName = loginUserName,
description = repository.repository.description,
isPrivate = repository.repository.isPrivate,
originRepositoryName = Some(originRepositoryName),
originUserName = Some(originUserName),
parentRepositoryName = Some(repository.name),
parentUserName = Some(repository.owner)
)
// Insert default labels
insertDefaultLabels(loginUserName, repository.name)
// clone repository actually
JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name),
getRepositoryDir(loginUserName, repository.name))
// Create Wiki repository
JGitUtil.cloneRepository(
getWikiRepositoryDir(repository.owner, repository.name),
getWikiRepositoryDir(loginUserName, repository.name))
// insert commit id
using(Git.open(getRepositoryDir(loginUserName, repository.name))){ git =>
JGitUtil.getRepositoryInfo(loginUserName, repository.name, baseUrl).branchList.foreach { branch =>
JGitUtil.getCommitLog(git, branch) match {
case Right((commits, _)) => commits.foreach { commit =>
if(!existsCommitId(loginUserName, repository.name, commit.id)){
insertCommitId(loginUserName, repository.name, commit.id)
}
}
case Left(_) => ???
}
}
}
// Record activity
recordForkActivity(repository.owner, repository.name, loginUserName)
// redirect to the repository
redirect(s"/${loginUserName}/${repository.name}")
}
}
}
})
private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
createLabel(userName, repositoryName, "bug", "fc2929")
createLabel(userName, repositoryName, "duplicate", "cccccc")
createLabel(userName, repositoryName, "enhancement", "84b6eb")
createLabel(userName, repositoryName, "invalid", "e6e6e6")
createLabel(userName, repositoryName, "question", "cc317c")
createLabel(userName, repositoryName, "wontfix", "ffffff")
}
private def existsAccount: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
}
private def uniqueRepository: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
params.get("owner").flatMap { userName =>
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
}
}
private def members: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = {
if(value.split(",").exists {
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
}) None else Some("Must select one manager at least.")
}
}
}

View File

@@ -36,18 +36,16 @@ abstract class ControllerBase extends ScalatraFilter
if(path.startsWith("/console/")){
val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
val baseUrl = this.baseUrl(httpRequest)
if(account == null){
// Redirect to login form
// TODO Should use the configured base url.
httpResponse.sendRedirect(context + "/signin?" + StringUtil.urlEncode(path))
httpResponse.sendRedirect(baseUrl + "/signin?redirect=" + StringUtil.urlEncode(path))
} else if(account.isAdmin){
// H2 Console (administrators only)
// TODO Should use the configured base url.
chain.doFilter(request, response)
} else {
// Redirect to dashboard
// TODO Should use the configured base url.
httpResponse.sendRedirect(context + "/")
httpResponse.sendRedirect(baseUrl + "/")
}
} else if(path.startsWith("/git/")){
// Git repository
@@ -68,7 +66,7 @@ abstract class ControllerBase extends ScalatraFilter
implicit def context: Context = {
contextCache.get match {
case null => {
val context = Context(loadSystemSettings().baseUrl.getOrElse(servletContext.getContextPath), LoginAccount, request)
val context = Context(loadSystemSettings(), LoginAccount, request)
contextCache.set(context)
context
}
@@ -138,10 +136,10 @@ abstract class ControllerBase extends ScalatraFilter
/**
* Context object for the current request.
*
* @param path the context path
*/
case class Context(path: String, loginAccount: Option[Account], request: HttpServletRequest){
case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){
lazy val path = settings.baseUrl.getOrElse(request.getServletContext.getContextPath)
lazy val currentPath = request.getRequestURI.substring(request.getContextPath.length)

View File

@@ -1,199 +0,0 @@
package app
import util.Directory._
import util.ControlUtil._
import util._
import service._
import org.eclipse.jgit.api.Git
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.lib.{FileMode, Constants}
import org.eclipse.jgit.dircache.DirCache
import org.scalatra.i18n.Messages
class CreateRepositoryController extends CreateRepositoryControllerBase
with RepositoryService with AccountService with WikiService with LabelsService with ActivityService
with UsersAuthenticator with ReadableUsersAuthenticator
/**
* Creates new repository.
*/
trait CreateRepositoryControllerBase extends ControllerBase {
self: RepositoryService with AccountService with WikiService with LabelsService with ActivityService
with UsersAuthenticator with ReadableUsersAuthenticator =>
case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
case class ForkRepositoryForm(owner: String, name: String)
val newForm = mapping(
"owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, unique))),
"description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())),
"createReadme" -> trim(label("Create README" , boolean()))
)(RepositoryCreationForm.apply)
val forkForm = mapping(
"owner" -> trim(label("Repository owner", text(required))),
"name" -> trim(label("Repository name", text(required)))
)(ForkRepositoryForm.apply)
/**
* Show the new repository form.
*/
get("/new")(usersOnly {
html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
})
/**
* Create new repository.
*/
post("/new", newForm)(usersOnly { form =>
LockUtil.lock(s"${form.owner}/${form.name}/create"){
if(getRepository(form.owner, form.name, baseUrl).isEmpty){
val ownerAccount = getAccountByUserName(form.owner).get
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
// Insert to the database at first
createRepository(form.name, form.owner, form.description, form.isPrivate)
// Add collaborators for group repository
if(ownerAccount.isGroupAccount){
getGroupMembers(form.owner).foreach { userName =>
addCollaborator(form.owner, form.name, userName)
}
}
// Insert default labels
insertDefaultLabels(form.owner, form.name)
// Create the actual repository
val gitdir = getRepositoryDir(form.owner, form.name)
JGitUtil.initRepository(gitdir)
if(form.createReadme){
using(Git.open(gitdir)){ git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
val content = if(form.description.nonEmpty){
form.name + "\n" +
"===============\n" +
"\n" +
form.description.get
} else {
form.name + "\n" +
"===============\n"
}
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
}
}
// Create Wiki repository
createWikiRepository(loginAccount, form.owner, form.name)
// Record activity
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
}
// redirect to the repository
redirect(s"/${form.owner}/${form.name}")
}
})
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
LockUtil.lock(s"${loginUserName}/${repository.name}/create"){
if(repository.owner == loginUserName){
// redirect to the repository
redirect(s"/${repository.owner}/${repository.name}")
} else {
getForkedRepositories(repository.owner, repository.name).find(_._1 == loginUserName).map { case (owner, name) =>
// redirect to the repository
redirect(s"/${owner}/${name}")
} getOrElse {
// Insert to the database at first
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
createRepository(
repositoryName = repository.name,
userName = loginUserName,
description = repository.repository.description,
isPrivate = repository.repository.isPrivate,
originRepositoryName = Some(originRepositoryName),
originUserName = Some(originUserName),
parentRepositoryName = Some(repository.name),
parentUserName = Some(repository.owner)
)
// Insert default labels
insertDefaultLabels(loginUserName, repository.name)
// clone repository actually
JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name),
getRepositoryDir(loginUserName, repository.name))
// Create Wiki repository
JGitUtil.cloneRepository(
getWikiRepositoryDir(repository.owner, repository.name),
getWikiRepositoryDir(loginUserName, repository.name))
// insert commit id
using(Git.open(getRepositoryDir(loginUserName, repository.name))){ git =>
JGitUtil.getRepositoryInfo(loginUserName, repository.name, baseUrl).branchList.foreach { branch =>
JGitUtil.getCommitLog(git, branch) match {
case Right((commits, _)) => commits.foreach { commit =>
if(!existsCommitId(loginUserName, repository.name, commit.id)){
insertCommitId(loginUserName, repository.name, commit.id)
}
}
case Left(_) => ???
}
}
}
// Record activity
recordForkActivity(repository.owner, repository.name, loginUserName)
// redirect to the repository
redirect(s"/${loginUserName}/${repository.name}")
}
}
}
})
private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
createLabel(userName, repositoryName, "bug", "fc2929")
createLabel(userName, repositoryName, "duplicate", "cccccc")
createLabel(userName, repositoryName, "enhancement", "84b6eb")
createLabel(userName, repositoryName, "invalid", "e6e6e6")
createLabel(userName, repositoryName, "question", "cc317c")
createLabel(userName, repositoryName, "wontfix", "ffffff")
}
private def existsAccount: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
}
/**
* Duplicate check for the repository name.
*/
private def unique: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
params.get("owner").flatMap { userName =>
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
}
}
}

View File

@@ -22,7 +22,6 @@ trait IndexControllerBase extends ControllerBase {
html.index(getRecentActivities(),
getVisibleRepositories(loginAccount, baseUrl),
loadSystemSettings(),
loginAccount.map{ account => getUserRepositories(account.userName, baseUrl) }.getOrElse(Nil)
)
}
@@ -32,11 +31,11 @@ trait IndexControllerBase extends ControllerBase {
if(redirect.isDefined && redirect.get.startsWith("/")){
flash += Keys.Flash.Redirect -> redirect.get
}
html.signin(loadSystemSettings())
html.signin()
}
post("/signin", form){ form =>
authenticate(loadSystemSettings(), form.userName, form.password) match {
authenticate(context.settings, form.userName, form.password) match {
case Some(account) => signin(account)
case None => redirect("/signin")
}

View File

@@ -79,7 +79,7 @@ trait PullRequestsControllerBase extends ControllerBase {
pulls.html.pullreq(
issue, pullreq,
getComments(owner, name, issueId),
getIssueLabels(owner, name, issueId.toInt),
getIssueLabels(owner, name, issueId),
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestonesWithIssueCount(owner, name),
getLabels(owner, name),
@@ -105,9 +105,9 @@ trait PullRequestsControllerBase extends ControllerBase {
} getOrElse NotFound
})
get("/:owner/:repository/pull/:id/delete/:branchName")(collaboratorsOnly { repository =>
get("/:owner/:repository/pull/:id/delete/*")(collaboratorsOnly { repository =>
params("id").toIntOpt.map { issueId =>
val branchName = params("branchName")
val branchName = multiParams("splat").head
val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
@@ -183,6 +183,18 @@ trait PullRequestsControllerBase extends ControllerBase {
}
}
// close issue by content of pull request
val defaultBranch = getRepository(owner, name, baseUrl).get.repository.defaultBranch
if(pullreq.branch == defaultBranch){
commits.flatten.foreach { commit =>
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
}
issue.content match {
case Some(content) => closeIssuesFromMessage(content, loginAccount.userName, owner, name)
case _ =>
}
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
}
// call web hook
getWebHookURLs(owner, name) match {
case webHookURLs if(webHookURLs.nonEmpty) =>

View File

@@ -82,44 +82,45 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
@scala.annotation.tailrec
def getPathObjectId(path: String, walk: TreeWalk): ObjectId = walk.next match {
case true if(walk.getPathString == path) => walk.getObjectId(0)
case true => getPathObjectId(path, walk)
def getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match {
case true if(walk.getPathString == path) => Some(walk.getObjectId(0))
case true => getPathObjectId(path, walk)
case false => None
}
val objectId = using(new TreeWalk(git.getRepository)){ treeWalk =>
using(new TreeWalk(git.getRepository)){ treeWalk =>
treeWalk.addTree(revCommit.getTree)
treeWalk.setRecursive(true)
getPathObjectId(path, treeWalk)
}
if(raw){
// Download
defining(JGitUtil.getContent(git, objectId, false).get){ bytes =>
contentType = FileUtil.getContentType(path, bytes)
bytes
}
} else {
// Viewer
val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize)
val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other"
val bytes = if(viewer == "other") JGitUtil.getContent(git, objectId, false) else None
val content = if(viewer == "other"){
if(bytes.isDefined && FileUtil.isText(bytes.get)){
// text
JGitUtil.ContentInfo("text", bytes.map(StringUtil.convertFromByteArray))
} else {
// binary
JGitUtil.ContentInfo("binary", None)
} map { objectId =>
if(raw){
// Download
defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes =>
contentType = FileUtil.getContentType(path, bytes)
bytes
}
} else {
// image or large
JGitUtil.ContentInfo(viewer, None)
}
// Viewer
val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize)
val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other"
val bytes = if(viewer == "other") JGitUtil.getContentFromId(git, objectId, false) else None
repo.html.blob(id, repository, path.split("/").toList, content, new JGitUtil.CommitInfo(revCommit))
}
val content = if(viewer == "other"){
if(bytes.isDefined && FileUtil.isText(bytes.get)){
// text
JGitUtil.ContentInfo("text", bytes.map(StringUtil.convertFromByteArray))
} else {
// binary
JGitUtil.ContentInfo("binary", None)
}
} else {
// image or large
JGitUtil.ContentInfo(viewer, None)
}
repo.html.blob(id, repository, path.split("/").toList, content, new JGitUtil.CommitInfo(revCommit))
}
} getOrElse NotFound
}
})
@@ -158,8 +159,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
/**
* Deletes branch.
*/
get("/:owner/:repository/delete/:branchName")(collaboratorsOnly { repository =>
val branchName = params("branchName")
get("/:owner/:repository/delete/*")(collaboratorsOnly { repository =>
val branchName = multiParams("splat").head
val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
@@ -180,8 +181,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
/**
* Download repository contents as an archive.
*/
get("/:owner/:repository/archive/:name")(referrersOnly { repository =>
val name = params("name")
get("/:owner/:repository/archive/*")(referrersOnly { repository =>
val name = multiParams("splat").head
if(name.endsWith(".zip")){
val revision = name.replaceFirst("\\.zip$", "")
@@ -192,7 +193,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
workDir.mkdirs
val zipFile = new File(workDir, repository.name + "-" +
(if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip")
(if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + ".zip")
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
@@ -207,7 +208,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
while(walk.next){
val name = walk.getPathString
val mode = walk.getFileMode(0)
if(mode != FileMode.TREE){
if(mode == FileMode.REGULAR_FILE || mode == FileMode.EXECUTABLE_FILE){
walk.getObjectId(objectId, 0)
val entry = new ZipEntry(name)
val loader = reader.open(objectId)
@@ -266,7 +267,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repo.html.guide(repository)
} else {
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
JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
defining(JGitUtil.getRevCommitFromId(git, objectId)){ revCommit =>
@@ -276,7 +277,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val readme = files.find { file =>
readmeFiles.contains(file.name.toLowerCase)
}.map { file =>
file -> StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
file -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId(
Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
}
repo.html.files(revision, repository,

View File

@@ -4,18 +4,21 @@ import service.{AccountService, SystemSettingsService}
import SystemSettingsService._
import util.AdminAuthenticator
import jp.sf.amateras.scalatra.forms._
import ssh.SshServer
class SystemSettingsController extends SystemSettingsControllerBase
with SystemSettingsService with AccountService with AdminAuthenticator
with AccountService with AdminAuthenticator
trait SystemSettingsControllerBase extends ControllerBase {
self: SystemSettingsService with AccountService with AdminAuthenticator =>
self: AccountService with AdminAuthenticator =>
private val form = mapping(
"baseUrl" -> trim(label("Base URL", optional(text()))),
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
"gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())),
"ssh" -> trim(label("SSH access", boolean())),
"sshPort" -> trim(label("SSH port", optional(number()))),
"smtp" -> optionalIfNotChecked("notification", mapping(
"host" -> trim(label("SMTP Host", text(required))),
"port" -> trim(label("SMTP Port", optional(number()))),
@@ -38,15 +41,28 @@ trait SystemSettingsControllerBase extends ControllerBase {
"tls" -> trim(label("Enable TLS", optional(boolean()))),
"keystore" -> trim(label("Keystore", optional(text())))
)(Ldap.apply))
)(SystemSettings.apply)
)(SystemSettings.apply).verifying { settings =>
if(settings.ssh && settings.baseUrl.isEmpty){
Seq("baseUrl" -> "Base URL is required if SSH access is enabled.")
} else Nil
}
get("/admin/system")(adminOnly {
admin.html.system(loadSystemSettings(), flash.get("info"))
admin.html.system(flash.get("info"))
})
post("/admin/system", form)(adminOnly { form =>
saveSystemSettings(form)
if(form.ssh && !SshServer.isActive && form.baseUrl.isDefined){
SshServer.start(request.getServletContext,
form.sshPort.getOrElse(SystemSettingsService.DefaultSshPort),
form.baseUrl.get)
} else if(!form.ssh && SshServer.isActive){
SshServer.stop()
}
flash += "info" -> "System settings has been updated."
redirect("/admin/system")
})

View File

@@ -5,6 +5,7 @@ import util.AdminAuthenticator
import util.StringUtil._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._
import org.scalatra.i18n.Messages
import org.apache.commons.io.FileUtils
import util.Directory._
@@ -23,10 +24,10 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
memberNames: Option[String])
members: String)
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
memberNames: Option[String], clearImage: Boolean, isRemoved: Boolean)
members: String, clearImage: Boolean, isRemoved: Boolean)
val newUserForm = mapping(
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
@@ -51,28 +52,28 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
)(EditUserForm.apply)
val newGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"memberNames" -> trim(label("Member Names" ,optional(text())))
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members)))
)(NewGroupForm.apply)
val editGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"memberNames" -> trim(label("Member Names" ,optional(text()))),
"clearImage" -> trim(label("Clear image" ,boolean())),
"removed" -> trim(label("Disable" ,boolean()))
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members))),
"clearImage" -> trim(label("Clear image" ,boolean())),
"removed" -> trim(label("Disable" ,boolean()))
)(EditGroupForm.apply)
get("/admin/users")(adminOnly {
val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
val users = getAllUsers(includeRemoved)
val members = users.collect { case account if(account.isGroupAccount) =>
account.userName -> getGroupMembers(account.userName)
val users = getAllUsers(includeRemoved)
val members = users.collect { case account if(account.isGroupAccount) =>
account.userName -> getGroupMembers(account.userName).map(_.userName)
}.toMap
admin.users.html.list(users, members, includeRemoved)
})
@@ -127,7 +128,11 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
createGroup(form.groupName, form.url)
updateGroupMembers(form.groupName, form.memberNames.map(_.split(",").toList).getOrElse(Nil))
updateGroupMembers(form.groupName, form.members.split(",").map {
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList)
updateImage(form.groupName, form.fileId, false)
redirect("/admin/users")
})
@@ -139,7 +144,11 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
})
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
defining(params("groupName"), form.memberNames.map(_.split(",").toList).getOrElse(Nil)){ case (groupName, memberNames) =>
defining(params("groupName"), form.members.split(",").map {
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList){ case (groupName, members) =>
getAccountByUserName(groupName, true).map { account =>
updateGroup(groupName, form.url, form.isRemoved)
@@ -155,11 +164,11 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
}
} else {
// Update GROUP_MEMBER
updateGroupMembers(form.groupName, memberNames)
updateGroupMembers(form.groupName, members)
// Update COLLABORATOR for group repositories
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
removeCollaborators(form.groupName, repositoryName)
memberNames.foreach { userName =>
members.foreach { case (userName, isManager) =>
addCollaborator(form.groupName, repositoryName, userName)
}
}
@@ -172,8 +181,17 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
}
})
post("/admin/users/_usercheck")(adminOnly {
// TODO Move to other generic controller?
post("/admin/users/_usercheck"){
getAccountByUserName(params("userName")).isDefined
})
}
private def members: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = {
if(value.split(",").exists {
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
}) None else Some("Must select one manager at least.")
}
}
}

View File

@@ -5,10 +5,12 @@ import scala.slick.driver.H2Driver.simple._
object GroupMembers extends Table[GroupMember]("GROUP_MEMBER") {
def groupName = column[String]("GROUP_NAME", O PrimaryKey)
def userName = column[String]("USER_NAME", O PrimaryKey)
def * = groupName ~ userName <> (GroupMember, GroupMember.unapply _)
def isManager = column[Boolean]("MANAGER")
def * = groupName ~ userName ~ isManager <> (GroupMember, GroupMember.unapply _)
}
case class GroupMember(
groupName: String,
userName: String
userName: String,
isManager: Boolean
)

View File

@@ -0,0 +1,22 @@
package model
import scala.slick.driver.H2Driver.simple._
object SshKeys extends Table[SshKey]("SSH_KEY") {
def userName = column[String]("USER_NAME")
def sshKeyId = column[Int]("SSH_KEY_ID", O AutoInc)
def title = column[String]("TITLE")
def publicKey = column[String]("PUBLIC_KEY")
def ins = userName ~ title ~ publicKey returning sshKeyId
def * = userName ~ sshKeyId ~ title ~ publicKey <> (SshKey, SshKey.unapply _)
def byPrimaryKey(userName: String, sshKeyId: Int) = (this.userName is userName.bind) && (this.sshKeyId is sshKeyId.bind)
}
case class SshKey(
userName: String,
sshKeyId: Int,
title: String,
publicKey: String
)

View File

@@ -59,7 +59,7 @@ trait AccountService {
Query(Accounts) filter(t => (t.userName is userName.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false): Option[Account] =
Query(Accounts) filter(t => (t.mailAddress is mailAddress.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
Query(Accounts) filter(t => (t.mailAddress.toLowerCase is mailAddress.toLowerCase.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
def getAllUsers(includeRemoved: Boolean = true): List[Account] =
if(includeRemoved){
@@ -122,18 +122,17 @@ trait AccountService {
def updateGroup(groupName: String, url: Option[String], removed: Boolean): Unit =
Accounts.filter(_.userName is groupName.bind).map(t => t.url.? ~ t.removed).update(url, removed)
def updateGroupMembers(groupName: String, members: List[String]): Unit = {
def updateGroupMembers(groupName: String, members: List[(String, Boolean)]): Unit = {
Query(GroupMembers).filter(_.groupName is groupName.bind).delete
members.foreach { userName =>
GroupMembers insert GroupMember (groupName, userName)
members.foreach { case (userName, isManager) =>
GroupMembers insert GroupMember (groupName, userName, isManager)
}
}
def getGroupMembers(groupName: String): List[String] =
def getGroupMembers(groupName: String): List[GroupMember] =
Query(GroupMembers)
.filter(_.groupName is groupName.bind)
.sortBy(_.userName)
.map(_.userName)
.list
def getGroupsByUserName(userName: String): List[String] =

View File

@@ -313,6 +313,14 @@ trait IssuesService {
}.toList
}
def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String) = {
extractCloseId(message).foreach { issueId =>
for(issue <- getIssue(owner, repository, issueId) if !issue.closed){
createComment(owner, repository, userName, issue.issueId, "Close", "close")
updateClosed(owner, repository, issue.issueId, true)
}
}
}
}
object IssuesService {

View File

@@ -63,8 +63,9 @@ RepositorySearchService { self: IssuesService =>
val list = new ListBuffer[(String, String)]
while (treeWalk.next()) {
if(treeWalk.getFileMode(0) != FileMode.TREE){
JGitUtil.getContent(git, treeWalk.getObjectId(0), false).foreach { bytes =>
val mode = treeWalk.getFileMode(0)
if(mode == FileMode.REGULAR_FILE || mode == FileMode.EXECUTABLE_FILE){
JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).foreach { bytes =>
if(FileUtil.isText(bytes)){
val text = StringUtil.convertFromByteArray(bytes)
val lowerText = text.toLowerCase

View File

@@ -147,7 +147,8 @@ trait RepositoryService { self: AccountService =>
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
))
),
getRepositoryManagers(repository.userName))
}
}
@@ -162,7 +163,8 @@ trait RepositoryService { self: AccountService =>
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
))
),
getRepositoryManagers(repository.userName))
}
}
@@ -195,10 +197,18 @@ trait RepositoryService { self: AccountService =>
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
))
),
getRepositoryManagers(repository.userName))
}
}
private def getRepositoryManagers(userName: String): Seq[String] =
if(getAccountByUserName(userName).exists(_.isGroupAccount)){
getGroupMembers(userName).collect { case x if(x.isManager) => x.userName }
} else {
Seq(userName)
}
/**
* Updates the last activity date of the repository.
*/
@@ -278,21 +288,25 @@ trait RepositoryService { self: AccountService =>
object RepositoryService {
case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository,
case class RepositoryInfo(owner: String, name: String, httpUrl: String, repository: Repository,
issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int,
branchList: List[String], tags: List[util.JGitUtil.TagInfo]){
branchList: Seq[String], tags: Seq[util.JGitUtil.TagInfo], managers: Seq[String]){
lazy val host = """^https?://(.+?)(:\d+)?/""".r.findFirstMatchIn(httpUrl).get.group(1)
def sshUrl(port: Int, userName: String) = s"ssh://${userName}@${host}:${port}/${owner}/${name}.git"
/**
* Creates instance with issue count and pull request count.
*/
def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int) =
this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags)
def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, managers: Seq[String]) =
this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers)
/**
* Creates instance without issue count and pull request count.
*/
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int) =
this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags)
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) =
this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers)
}
case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode])

View File

@@ -1,7 +1,6 @@
package service
import model._
import service.SystemSettingsService.SystemSettings
/**
* This service is used for a view helper mainly.
@@ -9,28 +8,23 @@ import service.SystemSettingsService.SystemSettings
* It may be called many times in one request, so each method stores
* its result into the cache which available during a request.
*/
trait RequestCache {
def getSystemSettings()(implicit context: app.Context): SystemSettings =
context.cache("system_settings"){
new SystemSettingsService {}.loadSystemSettings()
}
trait RequestCache extends SystemSettingsService with AccountService with IssuesService {
def getIssue(userName: String, repositoryName: String, issueId: String)(implicit context: app.Context): Option[Issue] = {
context.cache(s"issue.${userName}/${repositoryName}#${issueId}"){
new IssuesService {}.getIssue(userName, repositoryName, issueId)
super.getIssue(userName, repositoryName, issueId)
}
}
def getAccountByUserName(userName: String)(implicit context: app.Context): Option[Account] = {
context.cache(s"account.${userName}"){
new AccountService {}.getAccountByUserName(userName)
super.getAccountByUserName(userName)
}
}
def getAccountByMailAddress(mailAddress: String)(implicit context: app.Context): Option[Account] = {
context.cache(s"account.${mailAddress}"){
new AccountService {}.getAccountByMailAddress(mailAddress)
super.getAccountByMailAddress(mailAddress)
}
}
}

View File

@@ -0,0 +1,19 @@
package service
import model._
import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession
trait SshKeyService {
def addPublicKey(userName: String, title: String, publicKey: String): Unit =
SshKeys.ins insert (userName, title, publicKey)
def getPublicKeys(userName: String): List[SshKey] =
Query(SshKeys).filter(_.userName is userName.bind).sortBy(_.sshKeyId).list
def deletePublicKey(userName: String, sshKeyId: Int): Unit =
SshKeys filter (_.byPrimaryKey(userName, sshKeyId)) delete
}

View File

@@ -15,10 +15,12 @@ trait SystemSettingsService {
def saveSystemSettings(settings: SystemSettings): Unit = {
defining(new java.util.Properties()){ props =>
settings.baseUrl.foreach(props.setProperty(BaseURL, _))
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
props.setProperty(Gravatar, settings.gravatar.toString)
props.setProperty(Notification, settings.notification.toString)
props.setProperty(Ssh, settings.ssh.toString)
settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
if(settings.notification) {
settings.smtp.foreach { smtp =>
props.setProperty(SmtpHost, smtp.host)
@@ -56,10 +58,12 @@ trait SystemSettingsService {
props.load(new java.io.FileInputStream(GitBucketConf))
}
SystemSettings(
getOptionValue(props, BaseURL, None),
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
getValue(props, AllowAccountRegistration, false),
getValue(props, Gravatar, true),
getValue(props, Notification, false),
getValue(props, Ssh, false),
getOptionValue(props, SshPort, Some(DefaultSshPort)),
if(getValue(props, Notification, false)){
Some(Smtp(
getValue(props, SmtpHost, ""),
@@ -102,6 +106,8 @@ object SystemSettingsService {
allowAccountRegistration: Boolean,
gravatar: Boolean,
notification: Boolean,
ssh: Boolean,
sshPort: Option[Int],
smtp: Option[Smtp],
ldapAuthentication: Boolean,
ldap: Option[Ldap])
@@ -127,6 +133,7 @@ object SystemSettingsService {
fromAddress: Option[String],
fromName: Option[String])
val DefaultSshPort = 29418
val DefaultSmtpPort = 25
val DefaultLdapPort = 389
@@ -134,6 +141,8 @@ object SystemSettingsService {
private val AllowAccountRegistration = "allow_account_registration"
private val Gravatar = "gravatar"
private val Notification = "notification"
private val Ssh = "ssh"
private val SshPort = "ssh.port"
private val SmtpHost = "smtp.host"
private val SmtpPort = "smtp.port"
private val SmtpUser = "smtp.user"

View File

@@ -87,7 +87,7 @@ object WebHookService {
refName,
commits.map { commit =>
val diffs = JGitUtil.getDiffs(git, commit.id, false)
val commitUrl = repositoryInfo.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/commit/" + commit.id
val commitUrl = repositoryInfo.httpUrl.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/commit/" + commit.id
WebHookCommit(
id = commit.id,
@@ -106,7 +106,7 @@ object WebHookService {
}.toList,
WebHookRepository(
name = repositoryInfo.name,
url = repositoryInfo.url,
url = repositoryInfo.httpUrl,
description = repositoryInfo.repository.description.getOrElse(""),
watchers = 0,
forks = repositoryInfo.forkedCount,

View File

@@ -15,6 +15,7 @@ import org.eclipse.jgit.patch._
import org.eclipse.jgit.api.errors.PatchFormatException
import scala.collection.JavaConverters._
import scala.Some
import service.RepositoryService.RepositoryInfo
object WikiService {
@@ -40,6 +41,10 @@ object WikiService {
*/
case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date)
def httpUrl(repository: RepositoryInfo) = repository.httpUrl.replaceFirst("\\.git\\Z", ".wiki.git")
def sshUrl(repository: RepositoryInfo, settings: SystemSettingsService.SystemSettings, userName: String) =
repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), userName).replaceFirst("\\.git\\Z", ".wiki.git")
}
trait WikiService {
@@ -234,7 +239,7 @@ trait WikiService {
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)
updated = JGitUtil.getContentFromId(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false)
}
}
}
@@ -268,35 +273,35 @@ trait WikiService {
*/
def deleteWikiPage(owner: String, repository: String, pageName: String,
committer: String, mailAddress: String, message: String): Unit = {
LockUtil.lock(s"${owner}/${repository}/wiki"){
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
var removed = false
LockUtil.lock(s"${owner}/${repository}/wiki"){
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
var removed = false
using(new RevWalk(git.getRepository)){ revWalk =>
using(new TreeWalk(git.getRepository)){ treeWalk =>
val index = treeWalk.addTree(revWalk.parseTree(headId))
treeWalk.setRecursive(true)
while(treeWalk.next){
val path = treeWalk.getPathString
val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
if(path != pageName + ".md"){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
} else {
removed = true
}
using(new RevWalk(git.getRepository)){ revWalk =>
using(new TreeWalk(git.getRepository)){ treeWalk =>
val index = treeWalk.addTree(revWalk.parseTree(headId))
treeWalk.setRecursive(true)
while(treeWalk.next){
val path = treeWalk.getPathString
val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
if(path != pageName + ".md"){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
} else {
removed = true
}
}
}
if(removed){
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message)
}
if(removed){
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message)
}
}
}
}
}
}

View File

@@ -50,6 +50,7 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
Version(1, 12),
Version(1, 11),
Version(1, 10),
Version(1, 9),
@@ -97,7 +98,7 @@ object AutoUpdate {
*/
def getCurrentVersion(): Version = {
if(versionFile.exists){
FileUtils.readFileToString(versionFile, "UTF-8").split("\\.") match {
FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match {
case Array(majorVersion, minorVersion) => {
versions.find { v =>
v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt

View File

@@ -8,7 +8,7 @@ import org.slf4j.LoggerFactory
import javax.servlet.ServletConfig
import javax.servlet.ServletContext
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import util.{StringUtil, Keys, JGitUtil, Directory}
import util.ControlUtil._
import util.Implicits._
@@ -23,7 +23,7 @@ import util.JGitUtil.CommitInfo
* This servlet provides only Git repository functionality.
* Authentication is provided by [[servlet.BasicAuthenticationFilter]].
*/
class GitRepositoryServlet extends GitServlet {
class GitRepositoryServlet extends GitServlet with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet])
@@ -47,7 +47,19 @@ class GitRepositoryServlet extends GitServlet {
super.init(config)
}
override def service(req: HttpServletRequest, res: HttpServletResponse): Unit = {
val agent = req.getHeader("USER-AGENT")
val index = req.getRequestURI.indexOf(".git")
if(index >= 0 && (agent == null || agent.toLowerCase.indexOf("git/") < 0)){
// redirect for browsers
val paths = req.getRequestURI.substring(0, index).split("/")
res.sendRedirect(baseUrl(req) + "/" + paths.dropRight(1).last + "/" + paths.last)
} else {
// response for git client
super.service(req, res)
}
}
}
class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService {
@@ -87,12 +99,16 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
commands.asScala.foreach { command =>
logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
val commits = command.getType match {
case ReceiveCommand.Type.DELETE => Nil
case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
}
val refName = command.getRefName.split("/")
val branchName = refName.drop(2).mkString("/")
val commits = if (refName(1) == "tags") {
Nil
} else {
command.getType match {
case ReceiveCommand.Type.DELETE => Nil
case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
}
}
// Extract new commit and apply issue comment
val newCommits = if(commits.size > 1000){
@@ -141,6 +157,14 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
}
}
// close issues
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch
if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){
git.log.addRange(command.getOldId, command.getNewId).call.asScala.foreach { commit =>
closeIssuesFromMessage(commit.getFullMessage, pusher, owner, repository)
}
}
// call web hook
getWebHookURLs(owner, repository) match {
case webHookURLs if(webHookURLs.nonEmpty) =>

View File

@@ -0,0 +1,130 @@
package ssh
import org.apache.sshd.server.{CommandFactory, Environment, ExitCallback, Command}
import org.slf4j.LoggerFactory
import java.io.{InputStream, OutputStream}
import util.ControlUtil._
import org.eclipse.jgit.api.Git
import util.Directory._
import org.eclipse.jgit.transport.{ReceivePack, UploadPack}
import org.apache.sshd.server.command.UnknownCommand
import servlet.{Database, CommitLogHook}
import service.{AccountService, RepositoryService, SystemSettingsService}
import org.eclipse.jgit.errors.RepositoryNotFoundException
import javax.servlet.ServletContext
object GitCommand {
val CommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+).git'\Z""".r
}
abstract class GitCommand(val context: ServletContext, val owner: String, val repoName: String) extends Command {
self: RepositoryService with AccountService =>
private val logger = LoggerFactory.getLogger(classOf[GitCommand])
protected var err: OutputStream = null
protected var in: InputStream = null
protected var out: OutputStream = null
protected var callback: ExitCallback = null
protected def runTask(user: String): Unit
private def newTask(user: String): Runnable = new Runnable {
override def run(): Unit = {
Database(context) withTransaction {
try {
runTask(user)
callback.onExit(0)
} catch {
case e: RepositoryNotFoundException =>
logger.info(e.getMessage)
callback.onExit(1, "Repository Not Found")
case e: Throwable =>
logger.error(e.getMessage, e)
callback.onExit(1)
}
}
}
}
override def start(env: Environment): Unit = {
val user = env.getEnv.get("USER")
val thread = new Thread(newTask(user))
thread.start()
}
override def destroy(): Unit = {}
override def setExitCallback(callback: ExitCallback): Unit = {
this.callback = callback
}
override def setErrorStream(err: OutputStream): Unit = {
this.err = err
}
override def setOutputStream(out: OutputStream): Unit = {
this.out = out
}
override def setInputStream(in: InputStream): Unit = {
this.in = in
}
protected def isWritableUser(username: String, repositoryInfo: RepositoryService.RepositoryInfo): Boolean =
getAccountByUserName(username) match {
case Some(account) => hasWritePermission(repositoryInfo.owner, repositoryInfo.name, Some(account))
case None => false
}
}
class GitUploadPack(context: ServletContext, owner: String, repoName: String, baseUrl: String) extends GitCommand(context, owner, repoName)
with RepositoryService with AccountService {
override protected def runTask(user: String): Unit = {
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo =>
if(!repositoryInfo.repository.isPrivate || isWritableUser(user, repositoryInfo)){
using(Git.open(getRepositoryDir(owner, repoName))) { git =>
val repository = git.getRepository
val upload = new UploadPack(repository)
upload.upload(in, out, err)
}
}
}
}
}
class GitReceivePack(context: ServletContext, owner: String, repoName: String, baseUrl: String) extends GitCommand(context, owner, repoName)
with SystemSettingsService with RepositoryService with AccountService {
override protected def runTask(user: String): Unit = {
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo =>
if(isWritableUser(user, repositoryInfo)){
using(Git.open(getRepositoryDir(owner, repoName))) { git =>
val repository = git.getRepository
val receive = new ReceivePack(repository)
if(!repoName.endsWith(".wiki")){
receive.setPostReceiveHook(new CommitLogHook(owner, repoName, user, baseUrl))
}
receive.receive(in, out, err)
}
}
}
}
}
class GitCommandFactory(context: ServletContext, baseUrl: String) extends CommandFactory {
private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory])
override def createCommand(command: String): Command = {
logger.debug(s"command: $command")
command match {
case GitCommand.CommandRegex("upload", owner, repoName) => new GitUploadPack(context, owner, repoName, baseUrl)
case GitCommand.CommandRegex("receive", owner, repoName) => new GitReceivePack(context, owner, repoName, baseUrl)
case _ => new UnknownCommand(command)
}
}
}

View File

@@ -0,0 +1,62 @@
package ssh
import org.apache.sshd.common.Factory
import org.apache.sshd.server.{Environment, ExitCallback, Command}
import java.io.{OutputStream, InputStream}
import org.eclipse.jgit.lib.Constants
import service.SystemSettingsService
class NoShell extends Factory[Command] with SystemSettingsService {
override def create(): Command = new Command() {
private var in: InputStream = null
private var out: OutputStream = null
private var err: OutputStream = null
private var callback: ExitCallback = null
override def start(env: Environment): Unit = {
val user = env.getEnv.get("USER")
val port = loadSystemSettings().sshPort.getOrElse(SystemSettingsService.DefaultSshPort)
val message =
"""
| Welcome to
| _____ _ _ ____ _ _
| / ____| (_) | | | _ \ | | | |
| | | __ _ | |_ | |_) | _ _ ___ | | __ ___ | |_
| | | |_ | | | | __| | _ < | | | | / __| | |/ / / _ \ | __|
| | |__| | | | | |_ | |_) | | |_| | | (__ | < | __/ | |_
| \_____| |_| \__| |____/ \__,_| \___| |_|\_\ \___| \__|
|
| Successfully SSH Access.
| But interactive shell is disabled.
|
| Please use:
|
| git clone ssh://%s@GITBUCKET_HOST:%d/OWNER/REPOSITORY_NAME.git
""".stripMargin.format(user, port).replace("\n", "\r\n") + "\r\n"
err.write(Constants.encode(message))
err.flush()
in.close()
out.close()
err.close()
callback.onExit(127)
}
override def destroy(): Unit = {}
override def setInputStream(in: InputStream): Unit = {
this.in = in
}
override def setOutputStream(out: OutputStream): Unit = {
this.out = out
}
override def setErrorStream(err: OutputStream): Unit = {
this.err = err
}
override def setExitCallback(callback: ExitCallback): Unit = {
this.callback = callback
}
}
}

View File

@@ -0,0 +1,23 @@
package ssh
import org.apache.sshd.server.PublickeyAuthenticator
import org.apache.sshd.server.session.ServerSession
import java.security.PublicKey
import service.SshKeyService
import servlet.Database
import javax.servlet.ServletContext
class PublicKeyAuthenticator(context: ServletContext) extends PublickeyAuthenticator with SshKeyService {
override def authenticate(username: String, key: PublicKey, session: ServerSession): Boolean = {
Database(context) withTransaction {
getPublicKeys(username).exists { sshKey =>
SshUtil.str2PublicKey(sshKey.publicKey) match {
case Some(publicKey) => key.equals(publicKey)
case _ => false
}
}
}
}
}

View File

@@ -0,0 +1,68 @@
package ssh
import javax.servlet.{ServletContext, ServletContextEvent, ServletContextListener}
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider
import org.slf4j.LoggerFactory
import util.Directory
import service.SystemSettingsService
import java.util.concurrent.atomic.AtomicBoolean
object SshServer {
private val logger = LoggerFactory.getLogger(SshServer.getClass)
private val server = org.apache.sshd.SshServer.setUpDefaultServer()
private val active = new AtomicBoolean(false)
private def configure(context: ServletContext, port: Int, baseUrl: String) = {
server.setPort(port)
server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(s"${Directory.GitBucketHome}/gitbucket.ser"))
server.setPublickeyAuthenticator(new PublicKeyAuthenticator(context))
server.setCommandFactory(new GitCommandFactory(context, baseUrl))
server.setShellFactory(new NoShell)
}
def start(context: ServletContext, port: Int, baseUrl: String) = {
if(active.compareAndSet(false, true)){
configure(context, port, baseUrl)
server.start()
logger.info(s"Start SSH Server Listen on ${server.getPort}")
}
}
def stop() = {
if(active.compareAndSet(true, false)){
server.stop(true)
}
}
def isActive = active.get
}
/*
* Start a SSH Server Daemon
*
* How to use:
* git clone ssh://username@host_or_ip:29418/owner/repository_name.git
*/
class SshServerListener extends ServletContextListener with SystemSettingsService {
override def contextInitialized(sce: ServletContextEvent): Unit = {
val settings = loadSystemSettings()
if(settings.ssh){
if(settings.baseUrl.isEmpty){
// TODO use logger?
println("Could not start SshServer because the baseUrl is not configured.")
} else {
SshServer.start(sce.getServletContext,
settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort),
settings.baseUrl.get)
}
}
}
override def contextDestroyed(sce: ServletContextEvent): Unit = {
if(loadSystemSettings().ssh){
SshServer.stop()
}
}
}

View File

@@ -0,0 +1,33 @@
package ssh
import java.security.PublicKey
import org.slf4j.LoggerFactory
import org.apache.commons.codec.binary.Base64
import org.eclipse.jgit.lib.Constants
import org.apache.sshd.common.util.{KeyUtils, Buffer}
object SshUtil {
private val logger = LoggerFactory.getLogger(SshUtil.getClass)
def str2PublicKey(key: String): Option[PublicKey] = {
// TODO RFC 4716 Public Key is not supported...
val parts = key.split(" ")
if (parts.size < 2) {
logger.debug(s"Invalid PublicKey Format: key")
return None
}
try {
val encodedKey = parts(1)
val decode = Base64.decodeBase64(Constants.encodeASCII(encodedKey))
Some(new Buffer(decode).getRawPublicKey)
} catch {
case e: Throwable =>
logger.debug(e.getMessage, e)
None
}
}
def fingerPrint(key: String): String = KeyUtils.getFingerPrint(str2PublicKey(key).get)
}

View File

@@ -29,7 +29,7 @@ trait OneselfAuthenticator { self: ControllerBase =>
/**
* Allows only the repository owner and administrators.
*/
trait OwnerAuthenticator { self: ControllerBase with RepositoryService =>
trait OwnerAuthenticator { self: ControllerBase with RepositoryService with AccountService =>
protected def ownerOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
protected def ownerOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
@@ -40,6 +40,9 @@ trait OwnerAuthenticator { self: ControllerBase with RepositoryService =>
context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(repository.owner == x.userName) => action(repository)
case Some(x) if(getGroupMembers(repository.owner).exists { member =>
member.userName == x.userName && member.isManager == true
}) => action(repository)
case _ => Unauthorized()
}
} getOrElse NotFound()
@@ -106,7 +109,7 @@ trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService =
}
/**
* Allows only the repository owner and administrators.
* Allows only the repository owner (or manager for group repository) and administrators.
*/
trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
@@ -155,3 +158,24 @@ trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService =
}
}
}
/**
* Allows only the group managers.
*/
trait GroupManagerAuthenticator { self: ControllerBase with AccountService =>
protected def managersOnly(action: => Any) = { authenticate(action) }
protected def managersOnly[T](action: T => Any) = (form: T) => { authenticate(action(form)) }
private def authenticate(action: => Any) = {
{
defining(request.paths){ paths =>
context.loginAccount match {
case Some(x) if(getGroupMembers(paths(0)).exists { member =>
member.userName == x.userName && member.isManager
}) => action
case _ => Unauthorized()
}
}
}
}
}

View File

@@ -16,7 +16,7 @@ object ControlUtil {
def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B =
try f(resource) finally {
if(resource != null){
allCatch {
ignoring(classOf[Throwable]) {
resource.close()
}
}

View File

@@ -11,17 +11,20 @@ import org.eclipse.jgit.revwalk.filter._
import org.eclipse.jgit.treewalk._
import org.eclipse.jgit.treewalk.filter._
import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.eclipse.jgit.errors.MissingObjectException
import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException}
import java.util.Date
import org.eclipse.jgit.api.errors.NoHeadException
import service.RepositoryService
import org.eclipse.jgit.dircache.DirCacheEntry
import org.slf4j.LoggerFactory
/**
* Provides complex JGit operations.
*/
object JGitUtil {
private val logger = LoggerFactory.getLogger(JGitUtil.getClass)
/**
* The repository data.
*
@@ -45,9 +48,10 @@ object JGitUtil {
* @param commitId the last commit id
* @param committer the last committer name
* @param mailAddress the committer's mail address
* @param linkUrl the url of submodule
*/
case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, time: Date, message: String, commitId: String,
committer: String, mailAddress: String)
committer: String, mailAddress: String, linkUrl: Option[String])
/**
* The commit data.
@@ -72,11 +76,7 @@ object JGitUtil {
rev.getFullMessage,
rev.getParents().map(_.name).toList)
val summary = defining(fullMessage.trim.indexOf("\n")){ i =>
defining(if(i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage){ firstLine =>
if(firstLine.length > shortMessage.length) shortMessage else firstLine
}
}
val summary = getSummaryMessage(fullMessage, shortMessage)
val description = defining(fullMessage.trim.indexOf("\n")){ i =>
if(i >= 0){
@@ -104,6 +104,15 @@ object JGitUtil {
*/
case class TagInfo(name: String, time: Date, id: String)
/**
* The submodule data
*
* @param name the module name
* @param path the path in the repository
* @param url the repository url of this module
*/
case class SubmoduleInfo(name: String, path: String, url: String)
/**
* Returns RevCommit from the commit or tag id.
*
@@ -128,7 +137,7 @@ object JGitUtil {
using(Git.open(getRepositoryDir(owner, repository))){ git =>
try {
// 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(10000).sum
RepositoryInfo(
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git",
@@ -152,7 +161,7 @@ object JGitUtil {
}
}
}
/**
* Returns the file list of the specified path.
*
@@ -162,7 +171,7 @@ object JGitUtil {
* @return HTML of the file list
*/
def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = {
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String)]
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])]
using(new RevWalk(git.getRepository)){ revWalk =>
val objectId = git.getRepository.resolve(revision)
@@ -195,22 +204,30 @@ object JGitUtil {
})
}
while (treeWalk.next()) {
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString))
// submodule
val linkUrl = if(treeWalk.getFileMode(0) == FileMode.GITLINK){
getSubmodules(git, revCommit.getTree).find(_.path == treeWalk.getPathString).map(_.url)
} else None
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl))
}
}
}
val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision)
list.map { case (objectId, fileMode, path, name) =>
FileInfo(
objectId,
fileMode == FileMode.TREE,
name,
commits(path).getCommitterIdent.getWhen,
commits(path).getShortMessage,
commits(path).getName,
commits(path).getCommitterIdent.getName,
commits(path).getCommitterIdent.getEmailAddress)
list.map { case (objectId, fileMode, path, name, linkUrl) =>
defining(commits(path)){ commit =>
FileInfo(
objectId,
fileMode == FileMode.TREE || fileMode == FileMode.GITLINK,
name,
commit.getCommitterIdent.getWhen,
getSummaryMessage(commit.getFullMessage, commit.getShortMessage),
commit.getName,
commit.getCommitterIdent.getName,
commit.getCommitterIdent.getEmailAddress,
linkUrl)
}
}.sortWith { (file1, file2) =>
(file1.isDirectory, file2.isDirectory) match {
case (true , false) => true
@@ -219,7 +236,18 @@ object JGitUtil {
}
}.toList
}
/**
* Returns the first line of the commit message.
*/
private def getSummaryMessage(fullMessage: String, shortMessage: String): String = {
defining(fullMessage.trim.indexOf("\n")){ i =>
defining(if(i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage){ firstLine =>
if(firstLine.length > shortMessage.length) shortMessage else firstLine
}
}
}
/**
* Returns the commit list of the specified branch.
*
@@ -325,27 +353,6 @@ object JGitUtil {
}.toMap
}
/**
* Get object content of the given id as String from the Git repository.
*
* @param git the Git object
* @param id the object id
* @param large if false then returns None for the large file
* @return the object or None if object does not exist
*/
def getContent(git: Git, id: ObjectId, large: Boolean): Option[Array[Byte]] = try {
val loader = git.getRepository.getObjectDatabase.open(id)
if(large == false && FileUtil.isLarge(loader.getSize)){
None
} else {
using(git.getRepository.getObjectDatabase){ db =>
Some(db.open(id).getBytes)
}
}
} catch {
case e: MissingObjectException => None
}
/**
* Returns the tuple of diff of the given commit and the previous commit id.
*/
@@ -377,7 +384,7 @@ object JGitUtil {
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))
JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray))
}))
}
(buffer.toList, None)
@@ -400,8 +407,8 @@ object JGitUtil {
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None)
} else {
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
JGitUtil.getContent(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
JGitUtil.getContent(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray))
JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray))
}
}.toList
}
@@ -494,4 +501,73 @@ object JGitUtil {
newHeadId.getName
}
/**
* Read submodule information from .gitmodules
*/
def getSubmodules(git: Git, tree: RevTree): List[SubmoduleInfo] = {
val repository = git.getRepository
getContentFromPath(git, tree, ".gitmodules", true).map { bytes =>
(try {
val config = new BlobBasedConfig(repository.getConfig(), bytes)
config.getSubsections("submodule").asScala.map { module =>
val path = config.getString("submodule", module, "path")
val url = config.getString("submodule", module, "url")
SubmoduleInfo(module, path, url)
}
} catch {
case e: ConfigInvalidException => {
logger.error("Failed to load .gitmodules file for " + repository.getDirectory(), e)
Nil
}
}).toList
} getOrElse Nil
}
/**
* Get object content of the given path as byte array from the Git repository.
*
* @param git the Git object
* @param revTree the rev tree
* @param path the path
* @param fetchLargeFile if false then returns None for the large file
* @return the byte array of content or None if object does not exist
*/
def getContentFromPath(git: Git, revTree: RevTree, path: String, fetchLargeFile: Boolean): Option[Array[Byte]] = {
@scala.annotation.tailrec
def getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match {
case true if(walk.getPathString == path) => Some(walk.getObjectId(0))
case true => getPathObjectId(path, walk)
case false => None
}
using(new TreeWalk(git.getRepository)){ treeWalk =>
treeWalk.addTree(revTree)
treeWalk.setRecursive(true)
getPathObjectId(path, treeWalk)
} flatMap { objectId =>
getContentFromId(git, objectId, fetchLargeFile)
}
}
/**
* Get object content of the given object id as byte array from the Git repository.
*
* @param git the Git object
* @param id the object id
* @param fetchLargeFile if false then returns None for the large file
* @return the byte array of content or None if object does not exist
*/
def getContentFromId(git: Git, id: ObjectId, fetchLargeFile: Boolean): Option[Array[Byte]] = try {
val loader = git.getRepository.getObjectDatabase.open(id)
if(fetchLargeFile == false && FileUtil.isLarge(loader.getSize)){
None
} else {
using(git.getRepository.getObjectDatabase){ db =>
Some(db.open(id).getBytes)
}
}
} catch {
case e: MissingObjectException => None
}
}

View File

@@ -31,7 +31,7 @@ object StringUtil {
/**
* 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..
* 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))
@@ -47,12 +47,21 @@ object StringUtil {
}
/**
* Extract issue id like ````#issueId``` from the given message.
* 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) }
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(message).matchData.map(_.group(2))
/**
* Extract close issue id like ```close #issueId ``` from the given message.
*
* @param message the message which may contains close command
* @return the iterator of issue id
*/
def extractCloseId(message: String): Iterator[String] =
"(?i)(?<!\\w)(?:fix(?:e[sd])?|resolve[sd]?|close[sd]?)\\s+#(\\d+)(?!\\w)".r.findAllIn(message).matchData.map(_.group(1))
}

View File

@@ -16,7 +16,7 @@ trait AvatarImageProvider { self: RequestCache =>
val src = if(mailAddress.isEmpty){
// by user name
getAccountByUserName(userName).map { account =>
if(account.image.isEmpty && getSystemSettings().gravatar){
if(account.image.isEmpty && context.settings.gravatar){
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}"""
} else {
s"""${context.path}/${account.userName}/_avatar"""
@@ -27,13 +27,13 @@ trait AvatarImageProvider { self: RequestCache =>
} else {
// by mail address
getAccountByMailAddress(mailAddress).map { account =>
if(account.image.isEmpty && getSystemSettings().gravatar){
if(account.image.isEmpty && context.settings.gravatar){
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}"""
} else {
s"""${context.path}/${account.userName}/_avatar"""
}
} getOrElse {
if(getSystemSettings().gravatar){
if(context.settings.gravatar){
s"""https://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}"""
} else {
s"""${context.path}/_unknown/_avatar"""

View File

@@ -45,7 +45,7 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
(text, text)
}
val url = repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/" + StringUtil.urlEncode(page)
val url = repository.httpUrl.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/" + StringUtil.urlEncode(page)
if(getWikiPage(repository.owner, repository.name, page).isDefined){
new Rendering(url, label)
@@ -104,7 +104,7 @@ class GitBucketHtmlSerializer(
if(!enableWikiLink || url.startsWith("http://") || url.startsWith("https://")){
url
} else {
repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/_blob/" + url
repository.httpUrl.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/_blob/" + url
}
}

View File

@@ -1,71 +1,62 @@
@(account: Option[model.Account], info: Option[Any])(implicit context: app.Context)
@(account: model.Account, info: Option[Any])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main((if(account.isDefined) "Edit your profile" else "Create your account")){
@if(account.isDefined){
<h3>Edit your profile</h3>
} else {
<h3>Create your account</h3>
}
@helper.html.information(info)
<form action="@if(account.isDefined){@url(account.get.userName)/_edit}else{@path/register}" method="POST" validate="true">
<div class="row-fluid">
<div class="span6">
@if(account.isEmpty){
<fieldset>
<label for="userName" class="strong">Username:</label>
<input type="text" name="userName" id="userName" value=""/>
<span id="error-userName" class="error"></span>
</fieldset>
}
@if(account.map(_.password.nonEmpty).getOrElse(true)){
<fieldset>
<label for="password" class="strong">
Password
@if(account.nonEmpty){
(input to change password)
}
:
</label>
<input type="password" name="password" id="password" value=""/>
<span id="error-password" class="error"></span>
</fieldset>
}
<fieldset>
<label for="fullName" class="strong">Full Name:</label>
<input type="text" name="fullName" id="fullName" value="@account.map(_.fullName)"/>
<span id="error-fullName" class="error"></span>
</fieldset>
<fieldset>
<label for="mailAddress" class="strong">Mail Address:</label>
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
<span id="error-mailAddress" class="error"></span>
</fieldset>
<fieldset>
<label for="url" class="strong">URL (optional):</label>
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
<span id="error-url" class="error"></span>
</fieldset>
</div>
<div class="span6">
<fieldset>
<label for="avatar" class="strong">Image (optional):</label>
@helper.html.uploadavatar(account)
</fieldset>
@html.main("Edit your profile"){
<div class="row-fluid">
<div class="span3">
@menu("profile", settings.ssh)
</div>
<div class="span9">
@helper.html.information(info)
<form action="@url(account.userName)/_edit" method="POST" validate="true">
<div class="box">
<div class="box-header">Profile</div>
<div class="box-content">
<div class="row-fluid">
<div class="span6">
@if(account.password.nonEmpty){
<fieldset>
<label for="password" class="strong">
Password (input to change password):
</label>
<input type="password" name="password" id="password" value=""/>
<span id="error-password" class="error"></span>
</fieldset>
}
<fieldset>
<label for="fullName" class="strong">Full Name:</label>
<input type="text" name="fullName" id="fullName" value="@account.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.mailAddress"/>
<span id="error-mailAddress" class="error"></span>
</fieldset>
<fieldset>
<label for="url" class="strong">URL (optional):</label>
<input type="text" name="url" id="url" style="width: 300px;" value="@account.url"/>
<span id="error-url" class="error"></span>
</fieldset>
</div>
<div class="span6">
<fieldset>
<label for="avatar" class="strong">Image (optional):</label>
@helper.html.uploadavatar(Some(account))
</fieldset>
</div>
</div>
<div style="margin-top: 20px;">
<div class="pull-right">
<a href="@path/@account.userName/_delete" class="btn btn-danger" id="delete">Delete account</a>
</div>
<input type="submit" class="btn btn-success" value="Save"/>
<a href="@url(account.userName)" class="btn">Cancel</a>
</div>
</div>
</div>
<fieldset class="margin">
@if(account.isDefined){
<div class="pull-right">
<a href="@path/@account.get.userName/_delete" class="btn btn-danger" id="delete">Delete account</a>
</div>
<input type="submit" class="btn btn-success" value="Save"/>
<a href="@url(account.get.userName)" class="btn">Cancel</a>
} else {
<input type="submit" class="btn btn-success" value="Create account"/>
}
</fieldset>
</form>
</div>
}
<script>
$(function(){

View File

@@ -0,0 +1,140 @@
@(account: Option[model.Account], members: List[model.GroupMember])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(if(account.isEmpty) "Create group" else "Edit group"){
<div>
<form id="form" method="post" action="@if(account.isEmpty){@path/groups/new} else {@path/@account.get.userName/_editgroup}" validate="true">
<div class="row-fluid">
<div class="span5">
<fieldset>
<label for="groupName" class="strong">Group name</label>
<div>
<span id="error-groupName" class="error"></span>
</div>
<input type="text" name="groupName" id="groupName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
</fieldset>
<fieldset>
<label class="strong">URL (Optional)</label>
<div>
<span id="error-url" class="error"></span>
</div>
<input type="text" name="url" id="url" style="width: 300px;" value="@account.map(_.url)"/>
</fieldset>
<fieldset>
<label for="avatar" class="strong">Image (Optional)</label>
@helper.html.uploadavatar(account)
</fieldset>
</div>
<div class="span7">
<fieldset>
<label class="strong">Members</label>
<ul id="member-list" class="collaborator">
</ul>
@helper.html.account("memberName", 200)
<input type="button" class="btn" value="Add" id="addMember"/>
<input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.isManager).mkString(",")"/>
<div>
<span class="error" id="error-members"></span>
</div>
</fieldset>
</div>
</div>
<fieldset class="margin">
@if(account.isDefined){
<div class="pull-right">
<a href="@url(account.get.userName)/_deletegroup" id="delete" class="btn btn-danger">Delete Group</a>
</div>
}
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create Group} else {Update Group}"/>
@if(account.isDefined){
<a href="@url(account.get.userName)" class="btn">Cancel</a>
}
</fieldset>
</form>
</div>
}
<script>
$(function(){
$('input[type=submit]').click(function(){
updateMembers();
});
$('#addMember').click(function(){
$('#error-memberName').text('');
var userName = $('#memberName').val();
// check empty
if($.trim(userName) == ''){
return false;
}
// check duplication
var exists = $('#member-list li').filter(function(){
return $(this).data('name') == userName;
}).length > 0;
if(exists){
$('#error-memberName').text('User has been already added.');
return false;
}
// check existence
$.post('@path/admin/users/_usercheck', {
'userName': userName
}, function(data, status){
if(data == 'true'){
addMemberHTML(userName, false);
} else {
$('#error-memberName').text('User does not exist.');
}
});
});
$(document).on('click', '.remove', function(){
$(this).parent().remove();
});
// Don't submit form by ENTER key
$('#memberName').keypress(function(e){
return !(e.keyCode == 13);
});
$('#delete').click(function(){
return confirm('Once you delete this group, there is no going back.\nAre you sure?');
});
@members.map { member =>
addMemberHTML('@member.userName', @member.isManager);
}
function addMemberHTML(userName, isManager){
var memberButton = $('<button type="button" class="btn btn-default btn-mini" value="false">Member</button>').data('name', userName);
if(!isManager){
memberButton.addClass('active');
}
var managerButton = $('<button type="button" class="btn btn-default btn-mini" value="true">Manager</button>').data('name', userName);
if(isManager){
managerButton.addClass('active');
}
$('#member-list').append($('<li>')
.data('name', userName)
.append($('<div class="btn-group is_manager" data-toggle="buttons-radio">')
.append(memberButton)
.append(managerButton))
.append(' ')
.append($('<a>').attr('href', '@path/' + userName).text(userName))
.append(' ')
.append($('<a href="#" class="remove pull-right">(remove)</a>')));
}
function updateMembers(){
var members = $('#member-list li').map(function(i, e){
var userName = $(e).data('name');
return userName + ':' + $('button.active').filter(function(i, e){
return $(e).data('name') == userName;
}).attr('value');
}).get().join(',');
$('#members').val(members);
}
});
</script>

View File

@@ -1,4 +1,5 @@
@(account: model.Account, groupNames: List[String], active: String)(body: Html)(implicit context: app.Context)
@(account: model.Account, groupNames: List[String], active: String,
isGroupManager: Boolean = false)(body: Html)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(account.userName){
@@ -41,6 +42,13 @@
</div>
</li>
}
@if(loginAccount.isDefined && account.isGroupAccount && isGroupManager){
<li class="pull-right">
<div class="button-group">
<a href="@url(account.userName)/_editgroup" class="btn">Edit Group</a>
</div>
</li>
}
</ul>
@body
</div>

View File

@@ -1,7 +1,7 @@
@(account: model.Account, members: List[String])(implicit context: app.Context)
@(account: model.Account, members: List[String], isGroupManager: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@main(account, Nil, "members"){
@main(account, Nil, "members", isGroupManager){
@if(members.isEmpty){
No members
} else {

View File

@@ -0,0 +1,14 @@
@(active: String, ssh: Boolean)(implicit context: app.Context)
@import context._
<div class="box">
<ul class="nav nav-tabs nav-stacked side-menu">
<li@if(active=="profile"){ class="active"}>
<a href="@path/@loginAccount.get.userName/_edit">Profile</a>
</li>
@if(ssh){
<li@if(active=="ssh"){ class="active"}>
<a href="@path/@loginAccount.get.userName/_ssh">SSH Keys</a>
</li>
}
</ul>
</div>

View File

@@ -1,7 +1,7 @@
@(groupNames: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@main("Create a New Repository"){
@html.main("Create a New Repository"){
<div style="width: 600px; margin: 10px auto;">
<form id="form" method="post" action="@path/new" validate="true">
<fieldset>

View File

@@ -0,0 +1,48 @@
@()(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Create your account"){
<h3>Create your account</h3>
<form action="@path/register" method="POST" validate="true">
<div class="row-fluid">
<div class="span6">
<fieldset>
<label for="userName" class="strong">Username:</label>
<input type="text" name="userName" id="userName" value=""/>
<span id="error-userName" class="error"></span>
</fieldset>
<fieldset>
<label for="password" class="strong">
Password:
</label>
<input type="password" name="password" id="password" value=""/>
<span id="error-password" class="error"></span>
</fieldset>
<fieldset>
<label for="fullName" class="strong">Full Name:</label>
<input type="text" name="fullName" id="fullName" value=""/>
<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=""/>
<span id="error-mailAddress" class="error"></span>
</fieldset>
<fieldset>
<label for="url" class="strong">URL (optional):</label>
<input type="text" name="url" id="url" style="width: 400px;" value=""/>
<span id="error-url" class="error"></span>
</fieldset>
</div>
<div class="span6">
<fieldset>
<label for="avatar" class="strong">Image (optional):</label>
@helper.html.uploadavatar(None)
</fieldset>
</div>
</div>
<fieldset class="margin">
<input type="submit" class="btn btn-success" value="Create account"/>
</fieldset>
</form>
}

View File

@@ -1,7 +1,9 @@
@(account: model.Account, groupNames: List[String], repositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
@(account: model.Account, groupNames: List[String],
repositories: List[service.RepositoryService.RepositoryInfo],
isGroupManager: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@main(account, groupNames, "repositories"){
@main(account, groupNames, "repositories", isGroupManager){
@if(repositories.isEmpty){
No repositories
} else {

View File

@@ -0,0 +1,45 @@
@(account: model.Account, sshKeys: List[model.SshKey])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("SSH Keys"){
<div class="row-fluid">
<div class="span3">
@menu("ssh", settings.ssh)
</div>
<div class="span9">
<div class="box">
<div class="box-header">SSH Keys</div>
<div class="box-content">
@if(sshKeys.isEmpty){
No keys
}
@sshKeys.zipWithIndex.map { case (key, i) =>
@if(i != 0){
<hr>
}
<strong>@key.title</strong> (@_root_.ssh.SshUtil.fingerPrint(key.publicKey))
<a href="@path/@account.userName/_ssh/delete/@key.sshKeyId" class="btn btn-mini btn-danger pull-right">Delete</a>
}
</div>
</div>
<form method="POST" action="@path/@account.userName/_ssh" validate="true">
<div class="box">
<div class="box-header">Add an SSH Key</div>
<div class="box-content">
<fieldset>
<label for="title" class="strong">Title</label>
<div><span id="error-title" class="error"></span></div>
<input type="text" name="title" id="title" style="width: 400px;"/>
</fieldset>
<fieldset>
<label for="publicKey" class="strong">Key</label>
<div><span id="error-publicKey" class="error"></span></div>
<textarea name="publicKey" id="publicKey" style="width: 600px; height: 250px;"></textarea>
</fieldset>
<input type="submit" class="btn btn-success" value="Add"/>
</div>
</div>
</form>
</div>
</div>
}

View File

@@ -1,4 +1,4 @@
@(settings: service.SystemSettingsService.SystemSettings, info: Option[Any])(implicit context: app.Context)
@(info: Option[Any])(implicit context: app.Context)
@import context._
@import util.Directory._
@import view.helpers._
@@ -22,9 +22,10 @@
<fieldset>
<div class="controls">
<input type="text" name="baseUrl" id="baseUrl" style="width: 400px" value="@settings.baseUrl"/>
<span id="error-baseUrl" class="error"></span>
</div>
</fieldset>
<p>
<p class="muted">
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.
@@ -56,6 +57,29 @@
</label>
</fieldset>
<!--====================================================================-->
<!-- SSH access -->
<!--====================================================================-->
<hr>
<label class="strong">SSH access</label>
<fieldset>
<label class="checkbox">
<input type="checkbox" id="ssh" name="ssh"@if(settings.ssh){ checked}/>
Enable SSH access to git repository
</label>
</fieldset>
<div class="form-horizontal ssh">
<div class="control-group">
<label class="control-label" for="sshPort">SSH Port</label>
<div class="controls">
<input type="text" id="sshPort" name="sshPort" value="@settings.sshPort"/>
<span id="error-sshPort" class="error"></span>
</div>
</div>
</div>
<p class="muted">
Base URL is required if SSH access is enabled.
</p>
<!--====================================================================-->
<!-- Authentication -->
<!--====================================================================-->
<hr>
@@ -206,6 +230,10 @@
}
<script>
$(function(){
$('#ssh').change(function(){
$('.ssh input').prop('disabled', !$(this).prop('checked'));
}).change();
$('#notification').change(function(){
$('.notification input').prop('disabled', !$(this).prop('checked'));
}).change();

View File

@@ -1,11 +1,11 @@
@(account: Option[model.Account], members: List[String])(implicit context: app.Context)
@(account: Option[model.Account], members: List[model.GroupMember])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(if(account.isEmpty) "New Group" else "Update Group"){
@admin.html.menu("users"){
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_newgroup} else {@path/admin/users/@account.get.userName/_editgroup}" validate="true">
<div class="row-fluid">
<div class="span7">
<div class="span5">
<fieldset>
<label for="groupName" class="strong">Group name</label>
<div>
@@ -24,29 +24,23 @@
<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" value="@account.map(_.url)"/>
</fieldset>
<fieldset>
<label for="avatar" class="strong">Image (Optional)</label>
@helper.html.uploadavatar(account)
</fieldset>
</div>
<div class="span5">
<div class="span7">
<fieldset>
<label class="strong">Members</label>
<ul id="members" class="collaborator">
@members.map { userName =>
<li data-name="@userName">
<a href="@path/@url(userName)">@userName</a>
<a href="#" class="remove">(remove)</a>
</li>
}
<ul id="member-list" class="collaborator">
</ul>
@helper.html.account("memberName", 200)
<input type="button" class="btn" value="Add" id="addMember"/>
<input type="hidden" id="memberNames" name="memberNames" value="@members.mkString(",")"/>
<input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.isManager).mkString(",")"/>
<div>
<span class="error" id="error-memberName"></span>
<span class="error" id="error-members"></span>
</div>
</fieldset>
</div>
@@ -60,6 +54,10 @@
}
<script>
$(function(){
$('input[type=submit]').click(function(){
updateMembers();
});
$('#addMember').click(function(){
$('#error-memberName').text('');
var userName = $('#memberName').val();
@@ -70,7 +68,7 @@ $(function(){
}
// check duplication
var exists = $('#members li').filter(function(){
var exists = $('#member-list li').filter(function(){
return $(this).data('name') == userName;
}).length > 0;
if(exists){
@@ -83,19 +81,7 @@ $(function(){
'userName': userName
}, function(data, status){
if(data == 'true'){
// add member
$('#members').append($('<li>')
.data('name', userName)
.append($('<a>').attr('href', '@path/' + userName).text(userName))
.append(' ')
.append($('<a>').attr('href', '#').addClass('remove').text('(remove)')));
$('#memberName').val('');
// update hidden value
var userNames = $('#members li').map(function(i, e){
return $(e).data('name');
}).get().join(',');
$('#memberNames').val(userNames);
addMemberHTML(userName, false);
} else {
$('#error-memberName').text('User does not exist.');
}
@@ -103,20 +89,47 @@ $(function(){
});
$(document).on('click', '.remove', function(){
// remove member
$(this).parent().remove();
// update hidden value
var userNames = $('#members li').map(function(i, e){
return $(e).data('name');
}).get().join(',');
$('#memberNames').val(userNames);
});
// Don't submit form by ENTER key
$('#memberName').keypress(function(e){
console.log(e.keyCode);
return !(e.keyCode == 13);
});
@members.map { member =>
addMemberHTML('@member.userName', @member.isManager);
}
function addMemberHTML(userName, isManager){
var memberButton = $('<button type="button" class="btn btn-default btn-mini" value="false">Member</button>').data('name', userName);
if(!isManager){
memberButton.addClass('active');
}
var managerButton = $('<button type="button" class="btn btn-default btn-mini" value="true">Manager</button>').data('name', userName);
if(isManager){
managerButton.addClass('active');
}
$('#member-list').append($('<li>')
.data('name', userName)
.append($('<div class="btn-group is_manager" data-toggle="buttons-radio">')
.append(memberButton)
.append(managerButton))
.append(' ')
.append($('<a>').attr('href', '@path/' + userName).text(userName))
.append(' ')
.append($('<a href="#" class="remove pull-right">(remove)</a>')));
}
function updateMembers(){
var members = $('#member-list li').map(function(i, e){
var userName = $(e).data('name');
return userName + ':' + $('button.active').filter(function(i, e){
return $(e).data('name') == userName;
}).attr('value');
}).get().join(',');
$('#members').val(members);
}
});
</script>

View File

@@ -6,7 +6,7 @@
<div class="pull-right">
<div class="input-prepend">
<a href="@path/@repository.owner/@repository.name/fork" class="btn" style="margin-bottom: 10px;">Fork</a>
<span class="add-on"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
<span class="add-on count"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
</div>
</div>
}
@@ -30,6 +30,9 @@
}
}
</div>
@repository.repository.description.map { description =>
<p>@description</p>
}
<table class="global-nav box-header">
<tr>
<th class="box-header@if(active=="code"){ active}">
@@ -53,7 +56,7 @@
<th class="box-header@if(active=="network"){ active}">
<a href="@url(repository)/network/members">Network</a>
</th>
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || loginAccount.get.userName == repository.owner)){
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){
<th class="box-header@if(active=="settings"){ active}">
<a href="@url(repository)/settings">Settings</a>
</th>

View File

@@ -1,5 +1,5 @@
@(id: String, value: String)(html: Html)
<div class="input-append">
@(id: String, value: String, prepend: Boolean = false)(html: Html)
<div class="input-append@if(prepend){ input-prepend}">
@html
<span id="@id" class="add-on btn" data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="icon-check"></i></span>
</div>

View File

@@ -1,6 +1,5 @@
@(activities: List[model.Activity],
recentRepositories: List[service.RepositoryService.RepositoryInfo],
systemSettings: service.SystemSettingsService.SystemSettings,
userRepositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
@import context._
@import view.helpers._
@@ -12,7 +11,7 @@
</div>
<div class="span4">
@if(loginAccount.isEmpty){
@signinform(systemSettings)
@signinform(settings)
} else {
<table class="table table-bordered">
<tr>

View File

@@ -33,7 +33,7 @@
<input type="hidden" name="milestoneId" value=""/>
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li>
@milestones.map { milestone =>
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
<li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
<i class="icon-while"></i> @milestone.title

View File

@@ -54,7 +54,7 @@
@if(hasWritePermission){
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li>
@milestones.map { case (milestone, _, _) =>
@milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
<li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
@helper.html.checkicon(Some(milestone.milestoneId) == issue.milestoneId) @milestone.title

View File

@@ -54,7 +54,11 @@
}
@if(loginAccount.isDefined){
<a href="@url(loginAccount.get.userName)" class="username menu">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</a>
<a href="@path/new" class="menu" data-toggle="tooltip" data-placement="bottom" title="Create a new repo"><i class="icon-plus"></i></a>
<a class="dropdown-toggle menu" data-toggle="dropdown" href="#"><i class="icon-plus"></i><span class="caret" style="vertical-align: middle;"></span></a>
<ul class="dropdown-menu">
<li><a href="@path/new">New repository</a></li>
<li><a href="@path/groups/new">New group</a></li>
</ul>
<a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user"></i></a>
@if(loginAccount.get.isAdmin){
<a href="@path/admin/users" class="menu" data-toggle="tooltip" data-placement="bottom" title="Administration"><i class="icon-wrench"></i></a>

View File

@@ -1,4 +1,4 @@
@(branchInfo: List[(String, java.util.Date)],
@(branchInfo: Seq[(String, java.util.Date)],
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._

View File

@@ -12,7 +12,7 @@
<div class="head">
<div class="pull-right">
@defining(repository.commitCount){ commitCount =>
<a href="@url(repository)/commits/@encodeRefName(branch)">@if(commitCount > 1000){ @commitCount+ } else { @commitCount } @plural(commitCount, "commit")</a>&nbsp;
<a href="@url(repository)/commits/@encodeRefName(branch)">@if(commitCount > 10000){ @commitCount+ } else { @commitCount } @plural(commitCount, "commit")</a>&nbsp;
}
</div>
<a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> /
@@ -55,14 +55,22 @@
<tr>
<td width="16">
@if(file.isDirectory){
<img src="@assets/common/images/folder.png"/>
@if(file.linkUrl.isDefined){
<img src="@assets/common/images/folder_link.png"/>
} else {
<img src="@assets/common/images/folder.png"/>
}
} else {
<img src="@assets/common/images/file.png"/>
}
</td>
<td>
@if(file.isDirectory){
<a href="@url(repository)/tree@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a>
@if(file.linkUrl.isDefined){
<a href="@file.linkUrl">@file.name</a>
} else {
<a href="@url(repository)/tree@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a>
}
} else {
<a href="@url(repository)/blob@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a>
}

View File

@@ -9,13 +9,13 @@ touch README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin @repository.url
git remote add origin @repository.httpUrl
git push -u origin master
</pre>
<h3 style="margin-top: 30px;">Push an existing repository from the command line</h3>
<pre>
git remote add origin @repository.url
git remote add origin @repository.httpUrl
git push -u origin master
</pre>
}

View File

@@ -22,8 +22,15 @@
<li@if(active=="branches"){ class="active"}><a href="@url(repository)/branches">Branches@if(repository.branchList.length > 0){ <span class="badge">@repository.branchList.length</span>}</a></li>
<li@if(active=="tags" ){ class="active"}><a href="@url(repository)/tags">Tags@if(repository.tags.length > 0){ <span class="badge">@repository.tags.length</span>}</a></li>
<li class="pull-right">
@helper.html.copy("repository-url-copy", repository.url){
<input type="text" value="@repository.url" id="repository-url" readonly>
@helper.html.copy("repository-url-copy", repository.httpUrl, true){
@if(settings.ssh && loginAccount.isDefined){
<div class="btn-group add-on" data-toggle="buttons-radio" style="padding: 0px; border-width: 0px;">
<button type="button" class="btn active" id="repository-url-http">HTTP</button><button type="button" class="btn" id="repository-url-ssh">SSH</button>
</div>
} else {
<span class="add-on">HTTP</span>
}
<input type="text" value="@repository.httpUrl" id="repository-url" style="width: 150px;" readonly>
}
</li>
<li class="pull-right" style="margin-right: 8px;">
@@ -32,3 +39,17 @@
</div>
</li>
</ul>
@if(settings.ssh && loginAccount.isDefined){
<script>
$(function(){
$('#repository-url-http').click(function(){
$('#repository-url').val('@repository.httpUrl');
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
});
$('#repository-url-ssh').click(function(){
$('#repository-url').val('@repository.sshUrl(settings.sshPort.getOrElse(service.SystemSettingsService.DefaultSshPort), loginAccount.get.userName)');
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
});
});
</script>
}

View File

@@ -13,6 +13,10 @@
<a href="@url(collaboratorName)">@collaboratorName</a>
@if(!isGroupRepository){
<a href="@url(repository)/settings/collaborators/remove?name=@collaboratorName" class="remove">(remove)</a>
} else {
@if(repository.managers.contains(collaboratorName)){
(Manager)
}
}
</li>
}

View File

@@ -1,7 +1,7 @@
@(systemSettings: service.SystemSettingsService.SystemSettings)(implicit context: app.Context)
@()(implicit context: app.Context)
@import context._
@main("Sign in"){
<div class="signin-form">
@signinform(systemSettings)
@signinform(settings)
</div>
}

View File

@@ -1,4 +1,6 @@
@(pages: List[String], repository: service.RepositoryService.RepositoryInfo, hasWritePermission: Boolean)(implicit context: app.Context)
@(pages: List[String],
repository: service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(s"Pages - ${repository.owner}/${repository.name}", Some(repository)){

View File

@@ -1,15 +1,36 @@
@(active: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@(active: String,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import service.WikiService._
@import view.helpers._
<ul class="nav nav-tabs">
<li@if(active == "home" ){ class="active"}><a href="@url(repository)/wiki">Home</a></li>
<li@if(active == "pages" ){ class="active"}><a href="@url(repository)/wiki/_pages">Pages</a></li>
<li@if(active == "history"){ class="active"}><a href="@url(repository)/wiki/_history">Wiki History</a></li>
<li class="pull-right">
@defining(repository.url.replaceFirst("\\.git$", ".wiki.git")){ repositoryUrl =>
@helper.html.copy("repository-url-copy", repositoryUrl){
<input type="text" value="@repositoryUrl" readonly id="repository-url">
@helper.html.copy("repository-url-copy", httpUrl(repository), true){
@if(settings.ssh && loginAccount.isDefined){
<div class="btn-group add-on" data-toggle="buttons-radio" style="padding: 0px; border-width: 0px;">
<button type="button" class="btn active" id="repository-url-http">HTTP</button><button type="button" class="btn" id="repository-url-ssh">SSH</button>
</div>
} else {
<span class="add-on">HTTP</span>
}
<input type="text" value="@httpUrl(repository)" readonly id="repository-url">
}
</li>
</ul>
@if(settings.ssh && loginAccount.isDefined){
<script>
$(function(){
$('#repository-url-http').click(function(){
$('#repository-url').val('@httpUrl(repository)');
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
});
$('#repository-url-ssh').click(function(){
$('#repository-url').val('@sshUrl(repository, settings, loginAccount.get.userName)');
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
});
});
</script>
}

View File

@@ -18,6 +18,7 @@
<listener-class>servlet.AutoUpdateListener</listener-class>
</listener>
<!-- ===================================================================== -->
<!-- Scalatra configuration -->
<!-- ===================================================================== -->
@@ -25,6 +26,13 @@
<listener-class>org.scalatra.servlet.ScalatraListener</listener-class>
</listener>
<!-- ===================================================================== -->
<!-- SSH Daemon initialized -->
<!-- ===================================================================== -->
<listener>
<listener-class>ssh.SshServerListener</listener-class>
</listener>
<!-- ===================================================================== -->
<!-- HTTP interface for Git repositories -->
<!-- ===================================================================== -->

View File

@@ -80,19 +80,13 @@ table.global-nav th a:link, table.global-nav th a:hover, table.global-nav th a:v
text-decoration: none;
}
div.input-prepend span.add-on {
div.input-prepend span.count {
background-color: white;
-webkit-border-radius: 0 4px 4px 0;
-moz-border-radius: 0 4px 4px 0;
border-radius: 0 4px 4px 0;
}
/*
div.input-prepend span.add-on a {
color: #333;
}
*/
/* ======================================================================== */
/* General Styles */
/* ======================================================================== */
@@ -464,10 +458,9 @@ ul#commit-file-list li.border {
li.L0, li.L1, li.L2, li.L3, li.L4, li.L5, li.L6, li.L7, li.L8, li.L9 {
list-style-type: decimal;
background: white;
}
li.L1, li.L3, li.L5, li.L7, li.L9 {
background: #f5f5f5;
border-left: 1px solid #E5E5E5;
padding-left: 10px;
color: rgba(0, 0, 0, 0.3);
}
pre.blob {

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

View File

@@ -34,3 +34,14 @@ $(function(){
// syntax highlighting by google-code-prettify
prettyPrint();
});
function displayErrors(data){
var i = 0;
$.each(data, function(key, value){
$('#error-' + key.split(".").join("_")).text(value);
if(i == 0){
$('#' + key).focus();
}
i++;
});
}

View File

@@ -35,12 +35,22 @@ table.diff {
table.diff tbody {
font-family:Courier, monospace
}
table.diff tbody tr:hover {
background-color:#F8EEC7;
}
table.diff tbody tr:hover th {
background-color:#F6E8B5;
}
table.diff tbody tr:hover td {
background-color:#F6E8B5;
}
table.diff tbody th {
font-family:verdana,arial,'Bitstream Vera Sans',helvetica,sans-serif;
background:#EED;
background-color:#FBFBFB;
font-size:11px;
font-weight:normal;
border:1px solid #BBC;
border-top:none; /* for overriding bootstrap */
color:#886;
padding:.3em .5em .1em 2em;
text-align:right;
@@ -58,6 +68,7 @@ table.diff tbody td {
padding:0px .4em;
padding-top:.4em;
vertical-align:top;
border-top: none;
}
table.diff .empty {
background-color:#DDD;
@@ -69,9 +80,10 @@ table.diff .delete {
background-color:#FFDDDD;
}
table.diff .skip {
background-color:#EFEFEF;
border:1px solid #AAA;
border-right:1px solid #BBC;
background-color: #F8F8FF;
}
table.diff .skip:before {
content: " ...";
}
table.diff .insert {
background-color:#DDFFDD

View File

@@ -2,8 +2,9 @@ package service
import org.specs2.mutable.Specification
import java.util.Date
import model.GroupMember
class AccountServiceServiceSpec extends Specification with ServiceSpecBase {
class AccountServiceSpec extends Specification with ServiceSpecBase {
"AccountService" should {
val RootMailAddress = "root@localhost"
@@ -63,9 +64,9 @@ class AccountServiceServiceSpec extends Specification with ServiceSpecBase {
AccountService.getGroupMembers(group1) must_== Nil
AccountService.getGroupsByUserName(user1) must_== Nil
AccountService.updateGroupMembers(group1, List(user1))
AccountService.updateGroupMembers(group1, List((user1, true)))
AccountService.getGroupMembers(group1) must_== List(user1)
AccountService.getGroupMembers(group1) must_== List(GroupMember(group1, user1, true))
AccountService.getGroupsByUserName(user1) must_== List(group1)
AccountService.updateGroupMembers(group1, Nil)

View File

@@ -0,0 +1,40 @@
package ssh
import org.specs2.mutable._
import org.specs2.mock.Mockito
import org.apache.sshd.server.command.UnknownCommand
import javax.servlet.ServletContext
class GitCommandFactorySpec extends Specification with Mockito {
val factory = new GitCommandFactory(mock[ServletContext], "http://localhost:8080")
"createCommand" should {
"returns GitReceivePack when command is git-receive-pack" in {
factory.createCommand("git-receive-pack '/owner/repo.git'").isInstanceOf[GitReceivePack] must beTrue
factory.createCommand("git-receive-pack '/owner/repo.wiki.git'").isInstanceOf[GitReceivePack] must beTrue
}
"returns GitUploadPack when command is git-upload-pack" in {
factory.createCommand("git-upload-pack '/owner/repo.git'").isInstanceOf[GitUploadPack] must beTrue
factory.createCommand("git-upload-pack '/owner/repo.wiki.git'").isInstanceOf[GitUploadPack] must beTrue
}
"returns UnknownCommand when command is not git-(upload|receive)-pack" in {
factory.createCommand("git- '/owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue
factory.createCommand("git-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue
factory.createCommand("git-a-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue
factory.createCommand("git-up-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue
factory.createCommand("\ngit-upload-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue
}
"returns UnknownCommand when git command has no valid arguments" in {
// must be: git-upload-pack '/owner/repository_name.git'
factory.createCommand("git-upload-pack").isInstanceOf[UnknownCommand] must beTrue
factory.createCommand("git-upload-pack /owner/repo.git").isInstanceOf[UnknownCommand] must beTrue
factory.createCommand("git-upload-pack 'owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue
factory.createCommand("git-upload-pack '/ownerrepo.git'").isInstanceOf[UnknownCommand] must beTrue
factory.createCommand("git-upload-pack '/owner/repo.wiki'").isInstanceOf[UnknownCommand] must beTrue
}
}
}

View File

@@ -35,4 +35,22 @@ class StringUtilSpec extends Specification {
StringUtil.sha1("abc") mustEqual "a9993e364706816aba3e25717850c26c9cd0d89d"
}
}
"extractIssueId" should {
"extract '#xxx' and return extracted id" in {
StringUtil.extractIssueId("(refs #123)").toSeq mustEqual Seq("123")
}
"returns Nil from message which does not contain #xxx" in {
StringUtil.extractIssueId("this is test!").toSeq mustEqual Nil
}
}
"extractCloseId" should {
"extract 'close #xxx' and return extracted id" in {
StringUtil.extractCloseId("(close #123)").toSeq mustEqual Seq("123")
}
"returns Nil from message which does not contain close command" in {
StringUtil.extractCloseId("(refs #123)").toSeq mustEqual Nil
}
}
}

View File

@@ -10,53 +10,58 @@ import twirl.api.Html
class AvatarImageProviderSpec extends Specification {
implicit val context = app.Context("", None, null)
"getAvatarImageHtml" should {
"show Gravatar image for no image account if gravatar integration is enabled" in {
val provider = new AvatarImageProviderImpl(Some(createAccount(None)), createSystemSettings(true))
implicit val context = app.Context(createSystemSettings(true), None, null)
val provider = new AvatarImageProviderImpl(Some(createAccount(None)))
provider.toHtml("user", 20).toString mustEqual
"<img src=\"https://www.gravatar.com/avatar/d41d8cd98f00b204e9800998ecf8427e?s=20\" class=\"avatar\" style=\"width: 20px; height: 20px;\" />"
}
"show uploaded image even if gravatar integration is enabled" in {
val provider = new AvatarImageProviderImpl(Some(createAccount(Some("icon.png"))), createSystemSettings(true))
implicit val context = app.Context(createSystemSettings(true), None, null)
val provider = new AvatarImageProviderImpl(Some(createAccount(Some("icon.png"))))
provider.toHtml("user", 20).toString mustEqual
"<img src=\"/user/_avatar\" class=\"avatar\" style=\"width: 20px; height: 20px;\" />"
}
"show local image for no image account if gravatar integration is disabled" in {
val provider = new AvatarImageProviderImpl(Some(createAccount(None)), createSystemSettings(false))
implicit val context = app.Context(createSystemSettings(false), None, null)
val provider = new AvatarImageProviderImpl(Some(createAccount(None)))
provider.toHtml("user", 20).toString mustEqual
"<img src=\"/user/_avatar\" class=\"avatar\" style=\"width: 20px; height: 20px;\" />"
}
"show Gravatar image for specified mail address if gravatar integration is enabled" in {
val provider = new AvatarImageProviderImpl(None, createSystemSettings(true))
implicit val context = app.Context(createSystemSettings(true), None, null)
val provider = new AvatarImageProviderImpl(None)
provider.toHtml("user", 20, "hoge@hoge.com").toString mustEqual
"<img src=\"https://www.gravatar.com/avatar/4712f9b0e63f56ad952ad387eaa23b9c?s=20\" class=\"avatar\" style=\"width: 20px; height: 20px;\" />"
}
"show unknown image for unknown user if gravatar integration is enabled" in {
val provider = new AvatarImageProviderImpl(None, createSystemSettings(true))
implicit val context = app.Context(createSystemSettings(true), None, null)
val provider = new AvatarImageProviderImpl(None)
provider.toHtml("user", 20).toString mustEqual
"<img src=\"/_unknown/_avatar\" class=\"avatar\" style=\"width: 20px; height: 20px;\" />"
}
"show unknown image for specified mail address if gravatar integration is disabled" in {
val provider = new AvatarImageProviderImpl(None, createSystemSettings(false))
implicit val context = app.Context(createSystemSettings(false), None, null)
val provider = new AvatarImageProviderImpl(None)
provider.toHtml("user", 20, "hoge@hoge.com").toString mustEqual
"<img src=\"/_unknown/_avatar\" class=\"avatar\" style=\"width: 20px; height: 20px;\" />"
}
"add tooltip if it's enabled" in {
val provider = new AvatarImageProviderImpl(None, createSystemSettings(false))
implicit val context = app.Context(createSystemSettings(false), None, null)
val provider = new AvatarImageProviderImpl(None)
provider.toHtml("user", 20, "hoge@hoge.com", true).toString mustEqual
"<img src=\"/_unknown/_avatar\" class=\"avatar\" style=\"width: 20px; height: 20px;\" data-toggle=\"tooltip\" title=\"user\"/>"
@@ -80,10 +85,12 @@ class AvatarImageProviderSpec extends Specification {
private def createSystemSettings(useGravatar: Boolean) =
SystemSettings(
baseUrl = None,
baseUrl = Some(""),
allowAccountRegistration = false,
gravatar = useGravatar,
notification = false,
ssh = false,
sshPort = None,
smtp = None,
ldapAuthentication = false,
ldap = None)
@@ -91,15 +98,13 @@ class AvatarImageProviderSpec extends Specification {
/**
* Adapter to test AvatarImageProviderImpl.
*/
class AvatarImageProviderImpl(account: Option[Account], settings: SystemSettings)
extends AvatarImageProvider with RequestCache {
class AvatarImageProviderImpl(account: Option[Account]) extends AvatarImageProvider with RequestCache {
def toHtml(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false)
(implicit context: app.Context): Html = getAvatarImageHtml(userName, size, mailAddress, tooltip)
override def getAccountByMailAddress(mailAddress: String)(implicit context: app.Context): Option[Account] = account
override def getAccountByUserName(userName: String)(implicit context: app.Context): Option[Account] = account
override def getSystemSettings()(implicit context: app.Context): SystemSettings = settings
}
}