mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-02 03:26:06 +01:00
Merge branch 'master' into asciidoctorj
Conflicts: src/main/scala/app/RepositoryViewerController.scala
This commit is contained in:
17
README.md
17
README.md
@@ -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,19 @@ 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
|
||||
|
||||
### 1.11 - 01 Mar 2014
|
||||
- Base URL for redirection, notification and repository URL box is configurable
|
||||
- Remove ```--https``` option because it's possible to substitute in the base url
|
||||
|
||||
@@ -1 +1 @@
|
||||
sbt.version=0.12.3
|
||||
sbt.version=0.13.1
|
||||
|
||||
@@ -31,12 +31,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",
|
||||
|
||||
@@ -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
BIN
sbt-launch-0.13.1.jar
Normal file
Binary file not shown.
2
sbt.bat
2
sbt.bat
@@ -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
2
sbt.sh
@@ -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 "$@"
|
||||
|
||||
11
src/main/resources/update/1_12.sql
Normal file
11
src/main/resources/update/1_12.sql
Normal 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);
|
||||
@@ -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, "/*")
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
// Don't set content type via Accept header.
|
||||
override def format(implicit request: HttpServletRequest) = ""
|
||||
|
||||
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
|
||||
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = try {
|
||||
val httpRequest = request.asInstanceOf[HttpServletRequest]
|
||||
val httpResponse = response.asInstanceOf[HttpServletResponse]
|
||||
val context = request.getServletContext.getContextPath
|
||||
@@ -36,15 +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
|
||||
httpResponse.sendRedirect(context + "/signin?" + StringUtil.urlEncode(path))
|
||||
httpResponse.sendRedirect(baseUrl + "/signin?redirect=" + StringUtil.urlEncode(path))
|
||||
} else if(account.isAdmin){
|
||||
// H2 Console (administrators only)
|
||||
chain.doFilter(request, response)
|
||||
} else {
|
||||
// Redirect to dashboard
|
||||
httpResponse.sendRedirect(context + "/")
|
||||
httpResponse.sendRedirect(baseUrl + "/")
|
||||
}
|
||||
} else if(path.startsWith("/git/")){
|
||||
// Git repository
|
||||
@@ -53,12 +54,25 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
// Scalatra actions
|
||||
super.doFilter(request, response, chain)
|
||||
}
|
||||
} finally {
|
||||
contextCache.remove();
|
||||
}
|
||||
|
||||
private val contextCache = new java.lang.ThreadLocal[Context]()
|
||||
|
||||
/**
|
||||
* Returns the context object for the request.
|
||||
*/
|
||||
implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, request)
|
||||
implicit def context: Context = {
|
||||
contextCache.get match {
|
||||
case null => {
|
||||
val context = Context(loadSystemSettings(), LoginAccount, request)
|
||||
contextCache.set(context)
|
||||
context
|
||||
}
|
||||
case context => context
|
||||
}
|
||||
}
|
||||
|
||||
private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount)
|
||||
|
||||
@@ -116,14 +130,18 @@ abstract class ControllerBase extends ScalatraFilter
|
||||
includeContextPath: Boolean = true, includeServletPath: Boolean = true)
|
||||
(implicit request: HttpServletRequest, response: HttpServletResponse) =
|
||||
if (path.startsWith("http")) path
|
||||
else baseUrl + url(path, params, includeContextPath, includeServletPath)
|
||||
else baseUrl + url(path, params, false, false, false)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Context object for the current request.
|
||||
*/
|
||||
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)
|
||||
|
||||
/**
|
||||
* Get object from cache.
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 =>
|
||||
@@ -228,16 +228,16 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2
|
||||
val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2
|
||||
|
||||
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
|
||||
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
|
||||
}
|
||||
} getOrElse NotFound
|
||||
}
|
||||
case _ => {
|
||||
using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
|
||||
JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) =>
|
||||
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
|
||||
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
|
||||
} getOrElse {
|
||||
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}")
|
||||
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,21 +267,21 @@ 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 =>
|
||||
// get files
|
||||
val files = JGitUtil.getFileList(git, revision, path)
|
||||
val parentPath = if (path == ".") Nil else path.split("/").toList
|
||||
// process README.md or README.markdown
|
||||
val readme = files.find { file =>
|
||||
readmeFiles.contains(file.name.toLowerCase)
|
||||
}.map { file =>
|
||||
val path = (file.name :: parentPath.reverse).reverse
|
||||
path -> StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
|
||||
}
|
||||
JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
|
||||
defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit =>
|
||||
// get files
|
||||
val files = JGitUtil.getFileList(git, revision, path)
|
||||
val parentPath = if (path == ".") Nil else path.split("/").toList
|
||||
// process README.md or README.markdown
|
||||
val readme = files.find { file =>
|
||||
readmeFiles.contains(file.name.toLowerCase)
|
||||
}.map { file =>
|
||||
val path = (file.name :: parentPath.reverse).reverse
|
||||
path -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId(
|
||||
Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
|
||||
}
|
||||
|
||||
repo.html.files(revision, repository,
|
||||
if(path == ".") Nil else path.split("/").toList, // current path
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
22
src/main/scala/model/SshKey.scala
Normal file
22
src/main/scala/model/SshKey.scala
Normal 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
|
||||
)
|
||||
@@ -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] =
|
||||
|
||||
@@ -8,7 +8,6 @@ import Q.interpolation
|
||||
import model._
|
||||
import util.Implicits._
|
||||
import util.StringUtil._
|
||||
import util.StringUtil
|
||||
|
||||
trait IssuesService {
|
||||
import IssuesService._
|
||||
@@ -120,16 +119,10 @@ trait IssuesService {
|
||||
// get issues and comment count and labels
|
||||
searchIssueQuery(repos, condition, filterUser, onlyPullRequest)
|
||||
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
|
||||
.leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
|
||||
.leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
|
||||
.map { case (((t1, t2), t3), t4) =>
|
||||
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?)
|
||||
}
|
||||
.sortBy(_._4) // labelName
|
||||
.sortBy { case (t1, commentCount, _,_,_) =>
|
||||
.sortBy { case (t1, t2) =>
|
||||
(condition.sort match {
|
||||
case "created" => t1.registeredDate
|
||||
case "comments" => commentCount
|
||||
case "comments" => t2.commentCount
|
||||
case "updated" => t1.updatedDate
|
||||
}) match {
|
||||
case sort => condition.direction match {
|
||||
@@ -139,6 +132,11 @@ trait IssuesService {
|
||||
}
|
||||
}
|
||||
.drop(offset).take(limit)
|
||||
.leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
|
||||
.leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
|
||||
.map { case (((t1, t2), t3), t4) =>
|
||||
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?)
|
||||
}
|
||||
.list
|
||||
.splitWith { (c1, c2) =>
|
||||
c1._1.userName == c2._1.userName &&
|
||||
@@ -316,7 +314,7 @@ trait IssuesService {
|
||||
}
|
||||
|
||||
def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String) = {
|
||||
StringUtil.extractCloseId(message).foreach { issueId =>
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
src/main/scala/service/SshKeyService.scala
Normal file
19
src/main/scala/service/SshKeyService.scala
Normal 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
|
||||
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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){
|
||||
|
||||
130
src/main/scala/ssh/GitCommand.scala
Normal file
130
src/main/scala/ssh/GitCommand.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
62
src/main/scala/ssh/NoShell.scala
Normal file
62
src/main/scala/ssh/NoShell.scala
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/main/scala/ssh/PublicKeyAuthenticator.scala
Normal file
23
src/main/scala/ssh/PublicKeyAuthenticator.scala
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
68
src/main/scala/ssh/SshServerListener.scala
Normal file
68
src/main/scala/ssh/SshServerListener.scala
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
33
src/main/scala/ssh/SshUtil.scala
Normal file
33
src/main/scala/ssh/SshUtil.scala
Normal 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)
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(){
|
||||
|
||||
140
src/main/twirl/account/group.scala.html
Normal file
140
src/main/twirl/account/group.scala.html
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
14
src/main/twirl/account/menu.scala.html
Normal file
14
src/main/twirl/account/menu.scala.html
Normal 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>
|
||||
@@ -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>
|
||||
48
src/main/twirl/account/register.scala.html
Normal file
48
src/main/twirl/account/register.scala.html
Normal 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>
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
45
src/main/twirl/account/ssh.scala.html
Normal file
45
src/main/twirl/account/ssh.scala.html
Normal 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>
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
}
|
||||
@@ -56,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -54,14 +54,18 @@
|
||||
}
|
||||
@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>
|
||||
}
|
||||
<a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a>
|
||||
} else {
|
||||
<a href="@path/signin" class="btn btn-last" id="signin">Sign in</a>
|
||||
<a href="@path/signin?redirect=@urlEncode(currentPath)" class="btn btn-last" id="signin">Sign in</a>
|
||||
}
|
||||
</div><!--/.nav-collapse -->
|
||||
</div>
|
||||
@@ -76,7 +80,6 @@
|
||||
$('#search').submit(function(){
|
||||
return $.trim($(this).find('input[name=query]').val()) != '';
|
||||
});
|
||||
$('#signin').attr('href', '@path/signin?redirect=' + encodeURIComponent(location.pathname + location.search + location.hash));
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -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._
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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)){
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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 -->
|
||||
<!-- ===================================================================== -->
|
||||
|
||||
@@ -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 {
|
||||
|
||||
BIN
src/main/webapp/assets/common/images/folder_link.png
Normal file
BIN
src/main/webapp/assets/common/images/folder_link.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 785 B |
@@ -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++;
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
40
src/test/scala/ssh/GitCommandSpec.scala
Normal file
40
src/test/scala/ssh/GitCommandSpec.scala
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user