Merge branch 'master' into asciidoctorj

Conflicts:
	src/main/scala/app/RepositoryViewerController.scala
This commit is contained in:
Tobias Roeser
2014-03-29 13:53:23 +01:00
73 changed files with 1694 additions and 599 deletions

View File

@@ -23,7 +23,7 @@ Following features are not implemented, but we will make them in the future rele
- File editing in repository viewer
- Comment for the changeset
- Network graph
- Statics
- Statistics
- Watch / Star
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki).
@@ -35,6 +35,8 @@ Installation
2. Deploy it to the Servlet 3.0 container such as Tomcat 7.x, Jetty 8.x, GlassFish 3.x or higher.
3. Access **http://[hostname]:[port]/gitbucket/** using your web browser.
If you are using Gitbucket behind a webserver please make sure you have increased the **client_max_body_size** (on nignx)
The default administrator account is **root** and password is **root**.
or you can start GitBucket by `java -jar gitbucket.war` without servlet container. In this case, GitBucket URL is **http://[hostname]:8080/**. You can specify following options.
@@ -58,6 +60,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

View File

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

View File

@@ -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",

View File

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

Binary file not shown.

BIN
sbt-launch-0.13.1.jar Normal file

Binary file not shown.

View File

@@ -1,2 +1,2 @@
set SCRIPT_DIR=%~dp0
java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar "%SCRIPT_DIR%\sbt-launch-0.12.3.jar" %*
java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.1.jar" %*

2
sbt.sh
View File

@@ -1 +1 @@
java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar `dirname $0`/sbt-launch-0.12.3.jar "$@"
java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.1.jar "$@"

View File

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

View File

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

View File

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

View File

@@ -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.

View File

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

View File

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

View File

@@ -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}")
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -122,18 +122,17 @@ trait AccountService {
def updateGroup(groupName: String, url: Option[String], removed: Boolean): Unit =
Accounts.filter(_.userName is groupName.bind).map(t => t.url.? ~ t.removed).update(url, removed)
def updateGroupMembers(groupName: String, members: List[String]): Unit = {
def updateGroupMembers(groupName: String, members: List[(String, Boolean)]): Unit = {
Query(GroupMembers).filter(_.groupName is groupName.bind).delete
members.foreach { userName =>
GroupMembers insert GroupMember (groupName, userName)
members.foreach { case (userName, isManager) =>
GroupMembers insert GroupMember (groupName, userName, isManager)
}
}
def getGroupMembers(groupName: String): List[String] =
def getGroupMembers(groupName: String): List[GroupMember] =
Query(GroupMembers)
.filter(_.groupName is groupName.bind)
.sortBy(_.userName)
.map(_.userName)
.list
def getGroupsByUserName(userName: String): List[String] =

View File

@@ -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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
@(settings: service.SystemSettingsService.SystemSettings, info: Option[Any])(implicit context: app.Context)
@(info: Option[Any])(implicit context: app.Context)
@import context._
@import util.Directory._
@import view.helpers._
@@ -22,9 +22,10 @@
<fieldset>
<div class="controls">
<input type="text" name="baseUrl" id="baseUrl" style="width: 400px" value="@settings.baseUrl"/>
<span id="error-baseUrl" class="error"></span>
</div>
</fieldset>
<p>
<p class="muted">
The base URL is used for redirect, notification email, git repository URL box and more.
If the base URL is empty, GitBucket generates URL from request information.
You can use this property to adjust URL difference between the reverse proxy and GitBucket.
@@ -56,6 +57,29 @@
</label>
</fieldset>
<!--====================================================================-->
<!-- SSH access -->
<!--====================================================================-->
<hr>
<label class="strong">SSH access</label>
<fieldset>
<label class="checkbox">
<input type="checkbox" id="ssh" name="ssh"@if(settings.ssh){ checked}/>
Enable SSH access to git repository
</label>
</fieldset>
<div class="form-horizontal ssh">
<div class="control-group">
<label class="control-label" for="sshPort">SSH Port</label>
<div class="controls">
<input type="text" id="sshPort" name="sshPort" value="@settings.sshPort"/>
<span id="error-sshPort" class="error"></span>
</div>
</div>
</div>
<p class="muted">
Base URL is required if SSH access is enabled.
</p>
<!--====================================================================-->
<!-- Authentication -->
<!--====================================================================-->
<hr>
@@ -206,6 +230,10 @@
}
<script>
$(function(){
$('#ssh').change(function(){
$('.ssh input').prop('disabled', !$(this).prop('checked'));
}).change();
$('#notification').change(function(){
$('.notification input').prop('disabled', !$(this).prop('checked'));
}).change();

View File

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

View File

@@ -6,7 +6,7 @@
<div class="pull-right">
<div class="input-prepend">
<a href="@path/@repository.owner/@repository.name/fork" class="btn" style="margin-bottom: 10px;">Fork</a>
<span class="add-on"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
<span class="add-on count"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
</div>
</div>
}
@@ -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>

View File

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

View File

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

View File

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

View File

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

View File

@@ -54,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>

View File

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

View File

@@ -55,14 +55,22 @@
<tr>
<td width="16">
@if(file.isDirectory){
<img src="@assets/common/images/folder.png"/>
@if(file.linkUrl.isDefined){
<img src="@assets/common/images/folder_link.png"/>
} else {
<img src="@assets/common/images/folder.png"/>
}
} else {
<img src="@assets/common/images/file.png"/>
}
</td>
<td>
@if(file.isDirectory){
<a href="@url(repository)/tree@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a>
@if(file.linkUrl.isDefined){
<a href="@file.linkUrl">@file.name</a>
} else {
<a href="@url(repository)/tree@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a>
}
} else {
<a href="@url(repository)/blob@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a>
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
}
}