Compare commits

..

6 Commits
1.13 ... 1.11.1

Author SHA1 Message Date
takezoe
7deea43c75 Update README.,md for 1.11.1 release 2014-03-05 22:15:22 +09:00
takezoe
8842e1fef2 Fix error when base url is configured. 2014-03-05 22:12:52 +09:00
takezoe
5090112c39 Replace Context#path with the base url if it's configured. 2014-03-05 19:22:36 +09:00
takezoe
d4a3e88f1a (refs #299)Fix redirection path in PullRequestsController 2014-03-05 16:25:08 +09:00
shimamoto
acada42d3f (refs #292) Fix to limit issue result before joins issue labels. 2014-03-05 15:03:24 +09:00
takezoe
b6cfbcd19c (refs #296)Fix redirection path generation again 2014-03-05 10:37:38 +09:00
281 changed files with 1435 additions and 231832 deletions

View File

@@ -3,15 +3,10 @@ GitBucket
GitBucket is the easily installable Github clone written with Scala.
[![Gitter chat](https://badges.gitter.im/takezoe/gitbucket.png)](https://gitter.im/takezoe/gitbucket)
Features
--------
The current version of GitBucket provides a basic features below:
- Public / Private Git repository (http access only)
- Repository viewer and online file editing
- Repository viewer (some advanced features such as online file editing are not implemented)
- Repository search (Code and Issues)
- Wiki
- Issues
@@ -25,9 +20,10 @@ The current version of GitBucket provides a basic features below:
Following features are not implemented, but we will make them in the future release!
- File editing in repository viewer
- Comment for the changeset
- Network graph
- Statistics
- Statics
- 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).
@@ -39,8 +35,6 @@ 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.
@@ -55,24 +49,6 @@ To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored i
For Installation on Windows Server with IIS see [this wiki page](https://github.com/takezoe/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo)
### Mac OS X
#### Installing Via Homebrew
$ brew install gitbucket
==> Downloading https://github.com/takezoe/gitbucket/releases/download/1.10/gitbucket.war
######################################################################## 100.0%
==> Caveats
Note: When using launchctl the port will be 8080.
To have launchd start gitbucket at login:
ln -sfv /usr/local/opt/gitbucket/*.plist ~/Library/LaunchAgents
Then to load gitbucket now:
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.gitbucket.plist
Or, if you don't want/need launchctl, you can just run:
java -jar /usr/local/opt/gitbucket/libexec/gitbucket.war
==> Summary
/usr/local/Cellar/gitbucket/1.10: 3 files, 42M, built in 11 seconds
#### Manual Installation
On OS X, copy the [gitbucket.plist](https://raw.github.com/takezoe/gitbucket/master/contrib/macosx/gitbucket.plist) file to `~/Library/LaunchAgents/`
Run the following commands in `Terminal` to
@@ -82,22 +58,6 @@ Run the following commands in `Terminal` to
Release Notes
--------
### 1.13 - 29 Apr 2014
- Direct file editing in the repository viewer using AceEditor
- File attachment for issues
- Atom feed for user activities
- Fix some bugs
### 1.12 - 29 Mar 2014
- SSH repository access is available
- Allow users can create and management their groups
- Git submodule support
- Close issues via commit messages
- Show repository description below the name on repository page
- Fix presentation of the source viewer
- Upgrade to sbt 0.13
- Fix some bugs
### 1.11.1 - 06 Mar 2014
- Bug fix

View File

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

View File

@@ -39,6 +39,9 @@ start() {
if [ $GITBUCKET_HOST ]; then
START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
fi
if [ $GITBUCKET_HTTPS ]; then
START_OPTS="${START_OPTS} --https=true"
fi
# Run the Java process
GITBUCKET_HOME="${GITBUCKET_HOME}" java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 &

View File

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

View File

@@ -15,7 +15,6 @@ object MyBuild extends Build {
"gitbucket",
file("."),
settings = Defaults.defaultSettings ++ ScalatraPlugin.scalatraWithJRebel ++ Seq(
sourcesInBase := false,
organization := Organization,
name := Name,
version := Version,
@@ -31,13 +30,12 @@ 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.14",
"jp.sf.amateras" %% "scalatra-forms" % "0.0.11",
"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.11.0",
"com.typesafe.slick" %% "slick" % "1.0.1",
"com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.3.173",

View File

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

BIN
sbt-launch-0.12.3.jar Normal file

Binary file not shown.

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 -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.1.jar" %*
java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar "%SCRIPT_DIR%\sbt-launch-0.12.3.jar" %*

2
sbt.sh
View File

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

View File

@@ -1,11 +0,0 @@
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

@@ -1 +0,0 @@
DROP TABLE COMMIT_LOG;

View File

@@ -1,6 +1,6 @@
import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter}
import app._
//import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
import org.scalatra._
import javax.servlet._
import java.util.EnumSet
@@ -20,6 +20,7 @@ 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, "/*")
@@ -28,6 +29,7 @@ class ScalatraBootstrap extends LifeCycle {
context.mount(new IssuesController, "/*")
context.mount(new PullRequestsController, "/*")
context.mount(new RepositorySettingsController, "/*")
context.mount(new ValidationJavaScriptProvider, "/assets/common/js/*")
// Create GITBUCKET_HOME directory if it does not exist
val dir = new java.io.File(_root_.util.Directory.GitBucketHome)

View File

@@ -1,26 +1,17 @@
package app
import service._
import util._
import util.{FileUtil, OneselfAuthenticator}
import util.StringUtil._
import util.Directory._
import util.ControlUtil._
import ssh.SshUtil
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 WikiService with LabelsService with SshKeyService
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
with AccountService with RepositoryService with ActivityService with OneselfAuthenticator
trait AccountControllerBase extends AccountManagementControllerBase {
self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator =>
self: AccountService with RepositoryService with ActivityService with OneselfAuthenticator =>
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String])
@@ -28,8 +19,6 @@ 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)))),
@@ -48,45 +37,6 @@ 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, validPublicKey)))
)(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.
*/
@@ -101,30 +51,18 @@ trait AccountControllerBase extends AccountManagementControllerBase {
getActivitiesByUser(userName, true))
// Members
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 }))
}
case "members" if(account.isGroupAccount) =>
_root_.account.html.members(account, getGroupMembers(account.userName))
// Repositories
case _ => {
val members = getGroupMembers(account.userName)
case _ =>
_root_.account.html.repositories(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
getVisibleRepositories(context.loginAccount, context.baseUrl, Some(userName)),
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
}
getVisibleRepositories(context.loginAccount, baseUrl, Some(userName)))
}
} getOrElse NotFound
}
get("/:userName.atom") {
val userName = params("userName")
contentType = "application/atom+xml; type=feed"
helper.xml.feed(getActivitiesByUser(userName, true))
}
get("/:userName/_avatar"){
val userName = params("userName")
getAccountByUserName(userName).flatMap(_.image).map { image =>
@@ -138,9 +76,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
get("/:userName/_edit")(oneselfOnly {
val userName = params("userName")
getAccountByUserName(userName).map { x =>
account.html.edit(x, flash.get("info"))
} getOrElse NotFound
getAccountByUserName(userName).map(x => account.html.edit(Some(x), flash.get("info"))) getOrElse NotFound
})
post("/:userName/_edit", editForm)(oneselfOnly { form =>
@@ -180,259 +116,22 @@ 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(context.settings.allowAccountRegistration){
if(loadSystemSettings().allowAccountRegistration){
if(context.loginAccount.isDefined){
redirect("/")
} else {
account.html.register()
account.html.edit(None, None)
}
} else NotFound
}
post("/register", newForm){ form =>
if(context.settings.allowAccountRegistration){
if(loadSystemSettings().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, context.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))
// 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.")
}
}
private def validPublicKey: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = SshUtil.str2PublicKey(value) match {
case Some(_) => None
case None => Some("Key is invalid.")
}
}
}

View File

@@ -11,7 +11,8 @@ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import model.Account
import service.{SystemSettingsService, AccountService}
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest}
import java.text.SimpleDateFormat
import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
import org.scalatra.i18n._
@@ -35,16 +36,18 @@ 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(baseUrl + "/signin?redirect=" + StringUtil.urlEncode(path))
// TODO Should use the configured base url.
httpResponse.sendRedirect(context + "/signin?" + StringUtil.urlEncode(path))
} else if(account.isAdmin){
// H2 Console (administrators only)
// TODO Should use the configured base url.
chain.doFilter(request, response)
} else {
// Redirect to dashboard
httpResponse.sendRedirect(baseUrl + "/")
// TODO Should use the configured base url.
httpResponse.sendRedirect(context + "/")
}
} else if(path.startsWith("/git/")){
// Git repository
@@ -65,7 +68,7 @@ abstract class ControllerBase extends ScalatraFilter
implicit def context: Context = {
contextCache.get match {
case null => {
val context = Context(loadSystemSettings(), LoginAccount, request)
val context = Context(loadSystemSettings().baseUrl.getOrElse(servletContext.getContextPath), LoginAccount, request)
contextCache.set(context)
context
}
@@ -135,13 +138,12 @@ abstract class ControllerBase extends ScalatraFilter
/**
* Context object for the current request.
*
* @param path the context path
*/
case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){
case class Context(path: String, loginAccount: Option[Account], request: HttpServletRequest){
val path = settings.baseUrl.getOrElse(request.getContextPath)
val currentPath = request.getRequestURI.substring(request.getContextPath.length)
val baseUrl = settings.baseUrl(request)
val host = new java.net.URL(baseUrl).getHost
lazy val currentPath = request.getRequestURI.substring(request.getContextPath.length)
/**
* Get object from cache.
@@ -163,7 +165,7 @@ case class Context(settings: SystemSettingsService.SystemSettings, loginAccount:
/**
* Base trait for controllers which manages account information.
*/
trait AccountManagementControllerBase extends ControllerBase {
trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase {
self: AccountService =>
protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit =
@@ -174,9 +176,9 @@ trait AccountManagementControllerBase extends ControllerBase {
}
} else {
fileId.map { fileId =>
val filename = "avatar." + FileUtil.getExtension(session.getAndRemove(Keys.Session.Upload(fileId)).get)
val filename = "avatar." + FileUtil.getExtension(getUploadedFilename(fileId).get)
FileUtils.moveFile(
new java.io.File(getTemporaryDir(session.getId), fileId),
getTemporaryFile(fileId),
new java.io.File(getUserUploadDir(userName), filename)
)
updateAvatarImage(userName, Some(filename))
@@ -196,3 +198,28 @@ trait AccountManagementControllerBase extends ControllerBase {
}
}
/**
* Base trait for controllers which needs file uploading feature.
*/
trait FileUploadControllerBase {
def generateFileId: String =
new SimpleDateFormat("yyyyMMddHHmmSSsss").format(new java.util.Date(System.currentTimeMillis))
def TemporaryDir(implicit session: HttpSession): java.io.File =
new java.io.File(GitBucketHome, s"tmp/_upload/${session.getId}")
def getTemporaryFile(fileId: String)(implicit session: HttpSession): java.io.File =
new java.io.File(TemporaryDir, fileId)
// def removeTemporaryFile(fileId: String)(implicit session: HttpSession): Unit =
// getTemporaryFile(fileId).delete()
def removeTemporaryFiles()(implicit session: HttpSession): Unit =
FileUtils.deleteDirectory(TemporaryDir)
def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] =
session.getAndRemove[String](Keys.Session.Upload(fileId))
}

View File

@@ -0,0 +1,199 @@
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

@@ -49,7 +49,7 @@ trait DashboardControllerBase extends ControllerBase {
)
val userName = context.loginAccount.get.userName
val repositories = getUserRepositories(userName, context.baseUrl).map(repo => repo.owner -> repo.name)
val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request)
//
@@ -80,7 +80,7 @@ trait DashboardControllerBase extends ControllerBase {
}.copy(repo = repository))
val userName = context.loginAccount.get.userName
val repositories = getUserRepositories(userName, context.baseUrl).map(repo => repo.owner -> repo.name)
val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request)

View File

@@ -1,42 +1,31 @@
package app
import util.{Keys, FileUtil}
import _root_.util.{Keys, FileUtil}
import util.ControlUtil._
import util.Directory._
import org.scalatra._
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport, FileItem}
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport}
import org.apache.commons.io.FileUtils
/**
* Provides Ajax based file upload functionality.
*
* This servlet saves uploaded file.
* This servlet saves uploaded file as temporary file and returns the unique id.
* You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id.
*/
class FileUploadController extends ScalatraServlet with FileUploadSupport {
class FileUploadController extends ScalatraServlet with FileUploadSupport with FileUploadControllerBase {
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
post("/image"){
execute { (file, fileId) =>
FileUtils.writeByteArrayToFile(new java.io.File(getTemporaryDir(session.getId), fileId), file.get)
session += Keys.Session.Upload(fileId) -> file.name
}
}
post("/image/:owner/:repository"){
execute { (file, fileId) =>
FileUtils.writeByteArrayToFile(new java.io.File(getAttachedDir(params("owner"), params("repository")), fileId), file.get)
}
}
private def execute(f: (FileItem, String) => Unit) = fileParams.get("file") match {
case Some(file) if(FileUtil.isImage(file.name)) =>
defining(FileUtil.generateFileId){ fileId =>
f(file, fileId)
fileParams.get("file") match {
case Some(file) if(FileUtil.isImage(file.name)) => defining(generateFileId){ fileId =>
FileUtils.writeByteArrayToFile(getTemporaryFile(fileId), file.get)
session += Keys.Session.Upload(fileId) -> file.name
Ok(fileId)
}
case _ => BadRequest
case None => BadRequest
}
}
}

View File

@@ -21,8 +21,9 @@ trait IndexControllerBase extends ControllerBase {
val loginAccount = context.loginAccount
html.index(getRecentActivities(),
getVisibleRepositories(loginAccount, context.baseUrl),
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl) }.getOrElse(Nil)
getVisibleRepositories(loginAccount, baseUrl),
loadSystemSettings(),
loginAccount.map{ account => getUserRepositories(account.userName, baseUrl) }.getOrElse(Nil)
)
}
@@ -31,11 +32,11 @@ trait IndexControllerBase extends ControllerBase {
if(redirect.isDefined && redirect.get.startsWith("/")){
flash += Keys.Flash.Redirect -> redirect.get
}
html.signin()
html.signin(loadSystemSettings())
}
post("/signin", form){ form =>
authenticate(context.settings, form.userName, form.password) match {
authenticate(loadSystemSettings(), form.userName, form.password) match {
case Some(account) => signin(account)
case None => redirect("/signin")
}
@@ -46,11 +47,6 @@ trait IndexControllerBase extends ControllerBase {
redirect("/")
}
get("/activities.atom"){
contentType = "application/atom+xml; type=feed"
helper.xml.feed(getRecentActivities())
}
/**
* Set account information into HttpSession and redirect.
*/

View File

@@ -118,7 +118,7 @@ trait IssuesControllerBase extends ControllerBase {
// notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
Notifier.msgIssue(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
}
redirect(s"/${owner}/${name}/issues/${issueId}")
@@ -273,12 +273,6 @@ trait IssuesControllerBase extends ControllerBase {
}
})
get("/:owner/:repository/_attached/:file")(referrersOnly { repository =>
defining(new java.io.File(Directory.getAttachedDir(repository.owner, repository.name), params("file"))){ file =>
if(file.exists) file else NotFound
}
})
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
@@ -348,13 +342,13 @@ trait IssuesControllerBase extends ControllerBase {
case f =>
content foreach {
f.toNotify(repository, issueId, _){
Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
Notifier.msgComment(s"${baseUrl}/${owner}/${name}/${
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}")
}
}
action foreach {
f.toNotify(repository, issueId, _){
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
}
}
}

View File

@@ -79,7 +79,7 @@ trait PullRequestsControllerBase extends ControllerBase {
pulls.html.pullreq(
issue, pullreq,
getComments(owner, name, issueId),
getIssueLabels(owner, name, issueId),
getIssueLabels(owner, name, issueId.toInt),
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestonesWithIssueCount(owner, name),
getLabels(owner, name),
@@ -100,18 +100,18 @@ trait PullRequestsControllerBase extends ControllerBase {
pulls.html.mergeguide(
checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId),
pullreq,
s"${context.baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
s"${baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
}
} getOrElse NotFound
})
get("/:owner/:repository/pull/:id/delete/*")(collaboratorsOnly { repository =>
get("/:owner/:repository/pull/:id/delete/:branchName")(collaboratorsOnly { repository =>
params("id").toIntOpt.map { issueId =>
val branchName = multiParams("splat").head
val branchName = params("branchName")
val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.branchDelete().setForce(true).setBranchNames(branchName).call()
git.branchDelete().setBranchNames(branchName).call()
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
}
}
@@ -177,18 +177,12 @@ trait PullRequestsControllerBase extends ControllerBase {
val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
// close issue by content of pull request
val defaultBranch = getRepository(owner, name, context.baseUrl).get.repository.defaultBranch
if(pullreq.branch == defaultBranch){
commits.flatten.foreach { commit =>
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
commits.flatten.foreach { commit =>
if(!existsCommitId(owner, name, commit.id)){
insertCommitId(owner, name, commit.id)
}
issue.content match {
case Some(content) => closeIssuesFromMessage(content, loginAccount.userName, owner, name)
case _ =>
}
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
}
// call web hook
getWebHookURLs(owner, name) match {
case webHookURLs if(webHookURLs.nonEmpty) =>
@@ -201,7 +195,7 @@ trait PullRequestsControllerBase extends ControllerBase {
// notifications
Notifier().toNotify(repository, issueId, "merge"){
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/pull/${issueId}")
}
redirect(s"/${owner}/${name}/pull/${issueId}")
@@ -214,7 +208,7 @@ trait PullRequestsControllerBase extends ControllerBase {
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(originUserName), Some(originRepositoryName)) => {
getRepository(originUserName, originRepositoryName, context.baseUrl).map { originRepository =>
getRepository(originUserName, originRepositoryName, baseUrl).map { originRepository =>
using(
Git.open(getRepositoryDir(originUserName, originRepositoryName)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
@@ -251,7 +245,7 @@ trait PullRequestsControllerBase extends ControllerBase {
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
}
};
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
originRepository <- getRepository(originOwner, originRepositoryName, baseUrl)
) yield {
using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
@@ -260,7 +254,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
val forkedId = JGitUtil.getForkedCommitId(oldGit, newGit,
val forkedId = getForkedCommitId(oldGit, newGit,
originRepository.owner, originRepository.name, originBranch,
forkedRepository.owner, forkedRepository.name, forkedBranch)
@@ -303,7 +297,7 @@ trait PullRequestsControllerBase extends ControllerBase {
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
}
};
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
originRepository <- getRepository(originOwner, originRepositoryName, baseUrl)
) yield {
using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
@@ -356,7 +350,7 @@ trait PullRequestsControllerBase extends ControllerBase {
// notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
Notifier.msgPullRequest(s"${baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
}
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
@@ -432,8 +426,24 @@ trait PullRequestsControllerBase extends ControllerBase {
(defaultOwner, value)
}
/**
* Extracts all repository names from [[service.RepositoryService.RepositoryTreeNode]] as flat list.
*/
private def getRepositoryNames(node: RepositoryTreeNode): List[String] =
node.owner :: node.children.map { child => getRepositoryNames(child) }.flatten
/**
* Returns the identifier of the root commit (or latest merge commit) of the specified branch.
*/
private def getForkedCommitId(oldGit: Git, newGit: Git, userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): String =
JGitUtil.getCommitLogs(newGit, requestBranch, true){ commit =>
existsCommitId(userName, repositoryName, commit.getName) && JGitUtil.getBranchesOfCommit(oldGit, commit.getName).contains(branch)
}.head.id
private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) =
requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = {
using(
Git.open(getRepositoryDir(userName, repositoryName)),
Git.open(getRepositoryDir(requestUserName, requestRepositoryName))
@@ -451,6 +461,7 @@ trait PullRequestsControllerBase extends ControllerBase {
(commits, diffs)
}
}
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
defining(repository.owner, repository.name){ case (owner, repoName) =>

View File

@@ -72,7 +72,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
repository.owner,
repository.name,
form.description,
if(repository.branchList.isEmpty) "master" else form.defaultBranch,
form.defaultBranch,
repository.repository.parentUserName.map { _ =>
repository.repository.isPrivate
} getOrElse form.isPrivate

View File

@@ -1,9 +1,8 @@
package app
import _root_.util.JGitUtil.CommitInfo
import util.Directory._
import util.Implicits._
import _root_.util.ControlUtil._
import util.ControlUtil._
import _root_.util._
import service._
import org.scalatra._
@@ -13,53 +12,17 @@ import org.eclipse.jgit.lib._
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.treewalk._
import java.util.zip.{ZipEntry, ZipOutputStream}
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.dircache.DirCache
import org.eclipse.jgit.revwalk.{RevCommit, RevWalk}
import scala.Some
class RepositoryViewerController extends RepositoryViewerControllerBase
class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator
/**
* The repository viewer.
*/
trait RepositoryViewerControllerBase extends ControllerBase {
trait RepositoryViewerControllerBase extends ControllerBase {
self: RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
case class EditorForm(
branch: String,
path: String,
content: String,
message: Option[String],
charset: String,
newFileName: String,
oldFileName: Option[String]
)
case class DeleteForm(
branch: String,
path: String,
message: Option[String],
fileName: String
)
val editorForm = mapping(
"branch" -> trim(label("Branch", text(required))),
"path" -> trim(label("Path", text())),
"content" -> trim(label("Content", text(required))),
"message" -> trim(label("Message", optional(text()))),
"charset" -> trim(label("Charset", text(required))),
"newFileName" -> trim(label("Filename", text(required))),
"oldFileName" -> trim(label("Old filename", optional(text())))
)(EditorForm.apply)
val deleteForm = mapping(
"branch" -> trim(label("Branch", text(required))),
"path" -> trim(label("Path", text())),
"message" -> trim(label("Message", optional(text()))),
"fileName" -> trim(label("Filename", text(required)))
)(DeleteForm.apply)
/**
* Returns converted HTML from Markdown for preview.
*/
@@ -108,68 +71,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
})
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head)
repo.html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
None, JGitUtil.ContentInfo("text", None, Some("UTF-8")))
})
get("/:owner/:repository/edit/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
getPathObjectId(git, path, revCommit).map { objectId =>
val paths = path.split("/")
repo.html.editor(branch, repository, paths.take(paths.size - 1).toList, Some(paths.last),
JGitUtil.getContentInfo(git, path, objectId))
} getOrElse NotFound
}
})
get("/:owner/:repository/remove/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
getPathObjectId(git, path, revCommit).map { objectId =>
val paths = path.split("/")
repo.html.delete(branch, repository, paths.take(paths.size - 1).toList, paths.last,
JGitUtil.getContentInfo(git, path, objectId))
} getOrElse NotFound
}
})
post("/:owner/:repository/create", editorForm)(collaboratorsOnly { (form, repository) =>
commitFile(repository, form.branch, form.path, Some(form.newFileName), None, form.content, form.charset,
form.message.getOrElse(s"Create ${form.newFileName}"))
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}"
}")
})
post("/:owner/:repository/update", editorForm)(collaboratorsOnly { (form, repository) =>
commitFile(repository, form.branch, form.path, Some(form.newFileName), form.oldFileName, form.content, form.charset,
if(form.oldFileName.exists(_ == form.newFileName)){
form.message.getOrElse(s"Update ${form.newFileName}")
} else {
form.message.getOrElse(s"Rename ${form.oldFileName.get} to ${form.newFileName}")
})
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}"
}")
})
post("/:owner/:repository/remove", deleteForm)(collaboratorsOnly { (form, repository) =>
commitFile(repository, form.branch, form.path, None, Some(form.fileName), "", "",
form.message.getOrElse(s"Delete ${form.fileName}"))
redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}${if(form.path.length == 0) "" else form.path}")
})
/**
* Displays the file content of the specified branch or commit.
*/
@@ -179,18 +80,46 @@ trait RepositoryViewerControllerBase extends ControllerBase {
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
getPathObjectId(git, path, revCommit).map { objectId =>
if(raw){
// Download
defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes =>
contentType = FileUtil.getContentType(path, bytes)
bytes
@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)
}
val objectId = 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)
}
} else {
repo.html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId),
new JGitUtil.CommitInfo(revCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount))
// image or large
JGitUtil.ContentInfo(viewer, None)
}
} getOrElse NotFound
repo.html.blob(id, repository, path.split("/").toList, content, new JGitUtil.CommitInfo(revCommit))
}
}
})
@@ -229,12 +158,12 @@ trait RepositoryViewerControllerBase extends ControllerBase {
/**
* Deletes branch.
*/
get("/:owner/:repository/delete/*")(collaboratorsOnly { repository =>
val branchName = multiParams("splat").head
get("/:owner/:repository/delete/:branchName")(collaboratorsOnly { repository =>
val branchName = params("branchName")
val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.branchDelete().setForce(true).setBranchNames(branchName).call()
git.branchDelete().setBranchNames(branchName).call()
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
}
}
@@ -251,8 +180,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
/**
* Download repository contents as an archive.
*/
get("/:owner/:repository/archive/*")(referrersOnly { repository =>
val name = multiParams("splat").head
get("/:owner/:repository/archive/:name")(referrersOnly { repository =>
val name = params("name")
if(name.endsWith(".zip")){
val revision = name.replaceFirst("\\.zip$", "")
@@ -263,7 +192,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
workDir.mkdirs
val zipFile = new File(workDir, repository.name + "-" +
(if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + ".zip")
(if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip")
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
@@ -278,7 +207,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
while(walk.next){
val name = walk.getPathString
val mode = walk.getFileMode(0)
if(mode == FileMode.REGULAR_FILE || mode == FileMode.EXECUTABLE_FILE){
if(mode != FileMode.TREE){
walk.getObjectId(objectId, 0)
val entry = new ZipEntry(name)
val loader = reader.open(objectId)
@@ -304,13 +233,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
getRepository(
repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name),
context.baseUrl),
baseUrl),
getForkedRepositories(
repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name)),
repository)
})
private def splitPath(repository: service.RepositoryService.RepositoryInfo, path: String): (String, String) = {
val id = repository.branchList.collectFirst {
case branch if(path == branch || path.startsWith(branch + "/")) => branch
@@ -322,11 +251,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
private val readmeFiles = view.helpers.renderableSuffixes.map(suffix => s"readme${suffix}") ++ Seq("readme.txt", "readme")
private val readmeFiles = Seq("readme.md", "readme.markdown")
/**
* Provides HTML of the file list.
*
*
* @param repository the repository information
* @param revstr the branch name or commit id(optional)
* @param path the directory path (optional)
@@ -334,99 +263,30 @@ trait RepositoryViewerControllerBase extends ControllerBase {
*/
private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = {
if(repository.commitCount == 0){
repo.html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
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
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)
file -> StringUtil.convertFromByteArray(JGitUtil.getContent(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
new JGitUtil.CommitInfo(revCommit), // latest commit
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount))
files, readme)
}
} getOrElse NotFound
}
}
}
private def commitFile(repository: service.RepositoryService.RepositoryInfo,
branch: String, path: String, newFileName: Option[String], oldFileName: Option[String],
content: String, charset: String, message: String) = {
val newPath = newFileName.map { newFileName => if(path.length == 0) newFileName else s"${path}/${newFileName}" }
val oldPath = oldFileName.map { oldFileName => if(path.length == 0) oldFileName else s"${path}/${oldFileName}" }
LockUtil.lock(s"${repository.owner}/${repository.name}"){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val loginAccount = context.loginAccount.get
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headName = s"refs/heads/${branch}"
val headTip = git.getRepository.resolve(s"refs/heads/${branch}")
JGitUtil.processTree(git, headTip){ (path, tree) =>
if(!newPath.exists(_ == path) && !oldPath.exists(_ == path)){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
}
}
newPath.foreach { newPath =>
builder.add(JGitUtil.createDirCacheEntry(newPath, FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes(charset))))
}
builder.finish()
val commitId = JGitUtil.createNewCommit(git, inserter, headTip, builder.getDirCache.writeTree(inserter),
loginAccount.fullName, loginAccount.mailAddress, message)
inserter.flush()
inserter.release()
// update refs
val refUpdate = git.getRepository.updateRef(headName)
refUpdate.setNewObjectId(commitId)
refUpdate.setForceUpdate(false)
refUpdate.setRefLogIdent(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
//refUpdate.setRefLogMessage("merged", true)
refUpdate.update()
// record activity
recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch,
List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))))
// TODO invoke hook
}
}
}
private def getPathObjectId(git: Git, path: String, revCommit: RevCommit): Option[ObjectId] = {
@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(revCommit.getTree)
treeWalk.setRecursive(true)
_getPathObjectId(path, treeWalk)
}
}
}

View File

@@ -4,21 +4,18 @@ import service.{AccountService, SystemSettingsService}
import SystemSettingsService._
import util.AdminAuthenticator
import jp.sf.amateras.scalatra.forms._
import ssh.SshServer
class SystemSettingsController extends SystemSettingsControllerBase
with AccountService with AdminAuthenticator
with SystemSettingsService with AccountService with AdminAuthenticator
trait SystemSettingsControllerBase extends ControllerBase {
self: AccountService with AdminAuthenticator =>
self: SystemSettingsService with 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()))),
@@ -41,32 +38,15 @@ trait SystemSettingsControllerBase extends ControllerBase {
"tls" -> trim(label("Enable TLS", optional(boolean()))),
"keystore" -> trim(label("Keystore", optional(text())))
)(Ldap.apply))
)(SystemSettings.apply).verifying { settings =>
if(settings.ssh && settings.baseUrl.isEmpty){
Seq("baseUrl" -> "Base URL is required if SSH access is enabled.")
} else Nil
}
)(SystemSettings.apply)
get("/admin/system")(adminOnly {
admin.html.system(flash.get("info"))
admin.html.system(loadSystemSettings(), flash.get("info"))
})
post("/admin/system", form)(adminOnly { form =>
saveSystemSettings(form)
if(form.ssh && SshServer.isActive && context.settings.sshPort != form.sshPort){
SshServer.stop()
}
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,7 +5,6 @@ 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._
@@ -24,10 +23,10 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
members: String)
memberNames: Option[String])
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
members: String, clearImage: Boolean, isRemoved: Boolean)
memberNames: Option[String], clearImage: Boolean, isRemoved: Boolean)
val newUserForm = mapping(
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
@@ -52,28 +51,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()))),
"members" -> trim(label("Members" ,text(required, members)))
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"memberNames" -> trim(label("Member Names" ,optional(text())))
)(NewGroupForm.apply)
val editGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members))),
"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()))),
"memberNames" -> trim(label("Member Names" ,optional(text()))),
"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).map(_.userName)
}.toMap
val users = getAllUsers(includeRemoved)
val members = users.collect { case account if(account.isGroupAccount) =>
account.userName -> getGroupMembers(account.userName)
}.toMap
admin.users.html.list(users, members, includeRemoved)
})
@@ -128,11 +127,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
createGroup(form.groupName, form.url)
updateGroupMembers(form.groupName, form.members.split(",").map {
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList)
updateGroupMembers(form.groupName, form.memberNames.map(_.split(",").toList).getOrElse(Nil))
updateImage(form.groupName, form.fileId, false)
redirect("/admin/users")
})
@@ -144,11 +139,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
})
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
defining(params("groupName"), form.members.split(",").map {
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList){ case (groupName, members) =>
defining(params("groupName"), form.memberNames.map(_.split(",").toList).getOrElse(Nil)){ case (groupName, memberNames) =>
getAccountByUserName(groupName, true).map { account =>
updateGroup(groupName, form.url, form.isRemoved)
@@ -164,11 +155,11 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
}
} else {
// Update GROUP_MEMBER
updateGroupMembers(form.groupName, members)
updateGroupMembers(form.groupName, memberNames)
// Update COLLABORATOR for group repositories
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
removeCollaborators(form.groupName, repositoryName)
members.foreach { case (userName, isManager) =>
memberNames.foreach { userName =>
addCollaborator(form.groupName, repositoryName, userName)
}
}
@@ -181,17 +172,8 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
}
})
// TODO Move to other generic controller?
post("/admin/users/_usercheck"){
post("/admin/users/_usercheck")(adminOnly {
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

@@ -13,6 +13,12 @@ object Activities extends Table[Activity]("ACTIVITY") with BasicTemplate {
def autoInc = userName ~ repositoryName ~ activityUserName ~ activityType ~ message ~ additionalInfo.? ~ activityDate returning activityId
}
object CommitLog extends Table[(String, String, String)]("COMMIT_LOG") with BasicTemplate {
def commitId = column[String]("COMMIT_ID")
def * = userName ~ repositoryName ~ commitId
def byPrimaryKey(userName: String, repositoryName: String, commitId: String) = byRepository(userName, repositoryName) && (this.commitId is commitId.bind)
}
case class Activity(
activityId: Int,
userName: String,

View File

@@ -5,12 +5,10 @@ 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 isManager = column[Boolean]("MANAGER")
def * = groupName ~ userName ~ isManager <> (GroupMember, GroupMember.unapply _)
def * = groupName ~ userName <> (GroupMember, GroupMember.unapply _)
}
case class GroupMember(
groupName: String,
userName: String,
isManager: Boolean
userName: String
)

View File

@@ -1,22 +0,0 @@
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

@@ -34,34 +34,19 @@ trait AccountService {
/**
* Authenticate by LDAP.
*/
private def ldapAuthentication(settings: SystemSettings, userName: String, password: String): Option[Account] = {
private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) = {
LDAPUtil.authenticate(settings.ldap.get, userName, password) match {
case Right(ldapUserInfo) => {
// Create or update account by LDAP information
getAccountByUserName(ldapUserInfo.userName, true) match {
case Some(x) if(!x.isRemoved) => {
updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName))
getAccountByUserName(ldapUserInfo.userName)
}
getAccountByUserName(userName, true) match {
case Some(x) if(!x.isRemoved) => updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName))
case Some(x) if(x.isRemoved) => {
logger.info(s"LDAP Authentication Failed: Account is already registered but disabled..")
defaultAuthentication(userName, password)
}
case None => getAccountByMailAddress(ldapUserInfo.mailAddress, true) match {
case Some(x) if(!x.isRemoved) => {
updateAccount(x.copy(fullName = ldapUserInfo.fullName))
getAccountByUserName(ldapUserInfo.userName)
}
case Some(x) if(x.isRemoved) => {
logger.info(s"LDAP Authentication Failed: Account is already registered but disabled..")
defaultAuthentication(userName, password)
}
case None => {
createAccount(ldapUserInfo.userName, "", ldapUserInfo.fullName, ldapUserInfo.mailAddress, false, None)
getAccountByUserName(ldapUserInfo.userName)
}
}
case None => createAccount(userName, "", ldapUserInfo.fullName, ldapUserInfo.mailAddress, false, None)
}
getAccountByUserName(userName)
}
case Left(errorMessage) => {
logger.info(s"LDAP Authentication Failed: ${errorMessage}")
@@ -74,7 +59,7 @@ trait AccountService {
Query(Accounts) filter(t => (t.userName is userName.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false): Option[Account] =
Query(Accounts) filter(t => (t.mailAddress.toLowerCase is mailAddress.toLowerCase.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
Query(Accounts) filter(t => (t.mailAddress is mailAddress.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
def getAllUsers(includeRemoved: Boolean = true): List[Account] =
if(includeRemoved){
@@ -98,14 +83,14 @@ trait AccountService {
isGroupAccount = false,
isRemoved = false)
def updateAccount(account: Account): Unit =
def updateAccount(account: Account): Unit =
Accounts
.filter { a => a.userName is account.userName.bind }
.map { a => a.password ~ a.fullName ~ a.mailAddress ~ a.isAdmin ~ a.url.? ~ a.registeredDate ~ a.updatedDate ~ a.lastLoginDate.? ~ a.removed }
.update (
account.password,
account.fullName,
account.mailAddress,
account.password,
account.fullName,
account.mailAddress,
account.isAdmin,
account.url,
account.registeredDate,
@@ -137,17 +122,18 @@ 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, Boolean)]): Unit = {
def updateGroupMembers(groupName: String, members: List[String]): Unit = {
Query(GroupMembers).filter(_.groupName is groupName.bind).delete
members.foreach { case (userName, isManager) =>
GroupMembers insert GroupMember (groupName, userName, isManager)
members.foreach { userName =>
GroupMembers insert GroupMember (groupName, userName)
}
}
def getGroupMembers(groupName: String): List[GroupMember] =
def getGroupMembers(groupName: String): List[String] =
Query(GroupMembers)
.filter(_.groupName is groupName.bind)
.sortBy(_.userName)
.map(_.userName)
.list
def getGroupsByUserName(userName: String): List[String] =

View File

@@ -153,6 +153,19 @@ trait ActivityService {
Some(message),
currentDate)
private def cut(value: String, length: Int): String =
def insertCommitId(userName: String, repositoryName: String, commitId: String) = {
CommitLog insert (userName, repositoryName, commitId)
}
def insertAllCommitIds(userName: String, repositoryName: String, commitIds: List[String]) =
CommitLog insertAll (commitIds.map(commitId => (userName, repositoryName, commitId)): _*)
def getAllCommitIds(userName: String, repositoryName: String): List[String] =
Query(CommitLog).filter(_.byRepository(userName, repositoryName)).map(_.commitId).list
def existsCommitId(userName: String, repositoryName: String, commitId: String): Boolean =
Query(CommitLog).filter(_.byPrimaryKey(userName, repositoryName, commitId)).firstOption.isDefined
private def cut(value: String, length: Int): String =
if(value.length > length) value.substring(0, length) + "..." else value
}

View File

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

View File

@@ -18,9 +18,6 @@ trait PullRequestService { self: IssuesService =>
def updateCommitIdTo(owner: String, repository: String, issueId: Int, commitIdTo: String): Unit =
Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).map(_.commitIdTo).update(commitIdTo)
def updateCommitIdFrom(owner: String, repository: String, issueId: Int, commitIdFrom: String): Unit =
Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).map(_.commitIdFrom).update(commitIdFrom)
def getPullRequestCountGroupByUser(closed: Boolean, owner: String, repository: Option[String]): List[PullRequestCount] =
Query(PullRequests)
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }

View File

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

View File

@@ -52,6 +52,7 @@ trait RepositoryService { self: AccountService =>
val issueComments = Query(IssueComments).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueLabels = Query(IssueLabels ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val collaborators = Query(Collaborators).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val commitLog = Query(CommitLog ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val activities = Query(Activities ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
Repositories.filter { t =>
@@ -77,6 +78,7 @@ trait RepositoryService { self: AccountService =>
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueLabels .insertAll(issueLabels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Collaborators .insertAll(collaborators .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
CommitLog .insertAll(commitLog .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
Activities .insertAll(activities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
// Update activity messages
@@ -100,6 +102,7 @@ trait RepositoryService { self: AccountService =>
def deleteRepository(userName: String, repositoryName: String): Unit = {
Activities .filter(_.byRepository(userName, repositoryName)).delete
CommitLog .filter(_.byRepository(userName, repositoryName)).delete
Collaborators .filter(_.byRepository(userName, repositoryName)).delete
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
Labels .filter(_.byRepository(userName, repositoryName)).delete
@@ -144,8 +147,7 @@ trait RepositoryService { self: AccountService =>
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
),
getRepositoryManagers(repository.userName))
))
}
}
@@ -160,8 +162,7 @@ trait RepositoryService { self: AccountService =>
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
),
getRepositoryManagers(repository.userName))
))
}
}
@@ -194,18 +195,10 @@ 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.
*/
@@ -285,25 +278,21 @@ trait RepositoryService { self: AccountService =>
object RepositoryService {
case class RepositoryInfo(owner: String, name: String, httpUrl: String, repository: Repository,
case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository,
issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int,
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"
branchList: List[String], tags: List[util.JGitUtil.TagInfo]){
/**
* Creates instance with issue count and pull request count.
*/
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)
def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int) =
this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags)
/**
* Creates instance without issue count and pull request count.
*/
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) =
this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers)
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int) =
this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags)
}
case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode])

View File

@@ -1,6 +1,7 @@
package service
import model._
import service.SystemSettingsService.SystemSettings
/**
* This service is used for a view helper mainly.
@@ -8,23 +9,28 @@ import model._
* 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 extends SystemSettingsService with AccountService with IssuesService {
trait RequestCache {
def getSystemSettings()(implicit context: app.Context): SystemSettings =
context.cache("system_settings"){
new SystemSettingsService {}.loadSystemSettings()
}
def getIssue(userName: String, repositoryName: String, issueId: String)(implicit context: app.Context): Option[Issue] = {
context.cache(s"issue.${userName}/${repositoryName}#${issueId}"){
super.getIssue(userName, repositoryName, issueId)
new IssuesService {}.getIssue(userName, repositoryName, issueId)
}
}
def getAccountByUserName(userName: String)(implicit context: app.Context): Option[Account] = {
context.cache(s"account.${userName}"){
super.getAccountByUserName(userName)
new AccountService {}.getAccountByUserName(userName)
}
}
def getAccountByMailAddress(mailAddress: String)(implicit context: app.Context): Option[Account] = {
context.cache(s"account.${mailAddress}"){
super.getAccountByMailAddress(mailAddress)
new AccountService {}.getAccountByMailAddress(mailAddress)
}
}
}

View File

@@ -1,19 +0,0 @@
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

@@ -7,16 +7,18 @@ import javax.servlet.http.HttpServletRequest
trait SystemSettingsService {
def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings().baseUrl(request)
def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings().baseUrl.getOrElse {
defining(request.getRequestURL.toString){ url =>
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
}
}.replaceFirst("/$", "")
def saveSystemSettings(settings: SystemSettings): Unit = {
defining(new java.util.Properties()){ props =>
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
settings.baseUrl.foreach(props.setProperty(BaseURL, _))
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)
@@ -54,12 +56,10 @@ trait SystemSettingsService {
props.load(new java.io.FileInputStream(GitBucketConf))
}
SystemSettings(
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
getOptionValue(props, BaseURL, None),
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,17 +102,9 @@ object SystemSettingsService {
allowAccountRegistration: Boolean,
gravatar: Boolean,
notification: Boolean,
ssh: Boolean,
sshPort: Option[Int],
smtp: Option[Smtp],
ldapAuthentication: Boolean,
ldap: Option[Ldap]){
def baseUrl(request: HttpServletRequest): String = baseUrl.getOrElse {
defining(request.getRequestURL.toString){ url =>
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
}
}.replaceFirst("/$", "")
}
ldap: Option[Ldap])
case class Ldap(
host: String,
@@ -135,7 +127,6 @@ object SystemSettingsService {
fromAddress: Option[String],
fromName: Option[String])
val DefaultSshPort = 29418
val DefaultSmtpPort = 25
val DefaultLdapPort = 389
@@ -143,8 +134,6 @@ 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.httpUrl.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/commit/" + commit.id
val commitUrl = repositoryInfo.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/commit/" + commit.id
WebHookCommit(
id = commit.id,
@@ -106,7 +106,7 @@ object WebHookService {
}.toList,
WebHookRepository(
name = repositoryInfo.name,
url = repositoryInfo.httpUrl,
url = repositoryInfo.url,
description = repositoryInfo.repository.description.getOrElse(""),
watchers = 0,
forks = repositoryInfo.forkedCount,

View File

@@ -15,7 +15,6 @@ 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 {
@@ -41,10 +40,6 @@ 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 {
@@ -175,9 +170,17 @@ trait WikiService {
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
JGitUtil.processTree(git, headId){ (path, tree) =>
if(revertInfo.find(x => x.filePath == path).isEmpty){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
using(new RevWalk(git.getRepository)){ revWalk =>
using(new TreeWalk(git.getRepository)){ treeWalk =>
val index = treeWalk.addTree(revWalk.parseTree(headId))
treeWalk.setRecursive(true)
while(treeWalk.next){
val path = treeWalk.getPathString
val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser])
if(revertInfo.find(x => x.filePath == path).isEmpty){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
}
}
}
}
@@ -218,14 +221,22 @@ trait WikiService {
var removed = false
if(headId != null){
JGitUtil.processTree(git, headId){ (path, tree) =>
if(path == currentPageName + ".md" && currentPageName != newPageName){
removed = true
} else if(path != newPageName + ".md"){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
} else {
created = false
updated = JGitUtil.getContentFromId(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(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 == currentPageName + ".md" && currentPageName != newPageName){
removed = true
} else if(path != newPageName + ".md"){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
} else {
created = false
updated = JGitUtil.getContent(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false)
}
}
}
}
}
@@ -246,7 +257,7 @@ trait WikiService {
message
})
Some(newHeadId.getName)
Some(newHeadId)
} else None
}
}
@@ -257,26 +268,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
JGitUtil.processTree(git, headId){ (path, tree) =>
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,8 +50,6 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
Version(1, 13),
Version(1, 12),
Version(1, 11),
Version(1, 10),
Version(1, 9),
@@ -99,7 +97,7 @@ object AutoUpdate {
*/
def getCurrentVersion(): Version = {
if(versionFile.exists){
FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match {
FileUtils.readFileToString(versionFile, "UTF-8").split("\\.") match {
case Array(majorVersion, minorVersion) => {
versions.find { v =>
v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt

View File

@@ -3,7 +3,6 @@ package servlet
import javax.servlet._
import javax.servlet.http._
import service.{SystemSettingsService, AccountService, RepositoryService}
import model.Account
import org.slf4j.LoggerFactory
import util.Implicits._
import util.ControlUtil._
@@ -39,12 +38,9 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
request.getHeader("Authorization") match {
case null => requireAuth(response)
case auth => decodeAuthHeader(auth).split(":") match {
case Array(username, password) => getWritableUser(username, password, repository) match {
case Some(account) => {
request.setAttribute(Keys.Request.UserName, account.userName)
chain.doFilter(req, wrappedResponse)
}
case None => requireAuth(response)
case Array(username, password) if(isWritableUser(username, password, repository)) => {
request.setAttribute(Keys.Request.UserName, username)
chain.doFilter(req, wrappedResponse)
}
case _ => requireAuth(response)
}
@@ -65,10 +61,10 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
}
}
private def getWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Option[Account] =
private def isWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Boolean =
authenticate(loadSystemSettings(), username, password) match {
case x @ Some(account) if(hasWritePermission(repository.owner, repository.name, x)) => x
case _ => None
case Some(account) => hasWritePermission(repository.owner, repository.name, Some(account))
case None => false
}
private def requireAuth(response: HttpServletResponse): Unit = {

View File

@@ -8,7 +8,7 @@ import org.slf4j.LoggerFactory
import javax.servlet.ServletConfig
import javax.servlet.ServletContext
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import javax.servlet.http.HttpServletRequest
import util.{StringUtil, Keys, JGitUtil, Directory}
import util.ControlUtil._
import util.Implicits._
@@ -16,7 +16,6 @@ import service._
import WebHookService._
import org.eclipse.jgit.api.Git
import util.JGitUtil.CommitInfo
import service.IssuesService.IssueSearchCondition
/**
* Provides Git repository via HTTP.
@@ -24,7 +23,7 @@ import service.IssuesService.IssueSearchCondition
* This servlet provides only Git repository functionality.
* Authentication is provided by [[servlet.BasicAuthenticationFilter]].
*/
class GitRepositoryServlet extends GitServlet with SystemSettingsService {
class GitRepositoryServlet extends GitServlet {
private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet])
@@ -48,19 +47,7 @@ class GitRepositoryServlet extends GitServlet with SystemSettingsService {
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 {
@@ -81,9 +68,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
logger.debug("repository:" + owner + "/" + repository)
if(!repository.endsWith(".wiki")){
val hook = new CommitLogHook(owner, repository, pusher, baseUrl(request))
receivePack.setPreReceiveHook(hook)
receivePack.setPostReceiveHook(hook)
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseUrl(request)))
}
receivePack
}
@@ -92,55 +77,43 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String) extends PostReceiveHook with PreReceiveHook
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String) extends PostReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
private var existIds: Seq[String] = Nil
def onPreReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
try {
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
existIds = JGitUtil.getAllCommitIds(git)
}
} catch {
case ex: Exception => {
logger.error(ex.toString, ex)
throw ex
}
}
}
def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
try {
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
commands.asScala.foreach { command =>
logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
val commits = command.getType match {
case ReceiveCommand.Type.DELETE => Nil
case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
}
val refName = command.getRefName.split("/")
val branchName = refName.drop(2).mkString("/")
val commits = if (refName(1) == "tags") {
Nil
// Extract new commit and apply issue comment
val newCommits = if(commits.size > 1000){
val existIds = getAllCommitIds(owner, repository)
commits.flatMap { commit =>
if(!existIds.contains(commit.id)){
createIssueComment(commit)
Some(commit)
} else None
}
} else {
command.getType match {
case ReceiveCommand.Type.DELETE => Nil
case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
commits.flatMap { commit =>
if(!existsCommitId(owner, repository, commit.id)){
createIssueComment(commit)
Some(commit)
} else None
}
}
// Retrieve all issue count in the repository
val issueCount =
countIssue(IssueSearchCondition(state = "open"), Map.empty, false, owner -> repository) +
countIssue(IssueSearchCondition(state = "closed"), Map.empty, false, owner -> repository)
// Extract new commit and apply issue comment
val newCommits = commits.flatMap { commit =>
if (!existIds.contains(commit.id)) {
if (issueCount > 0) {
createIssueComment(commit)
}
Some(commit)
} else None
}
// batch insert all new commit id
insertAllCommitIds(owner, repository, newCommits.map(_.id))
// record activity
if(refName(1) == "heads"){
@@ -168,17 +141,6 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
}
}
// close issues
if(issueCount > 0) {
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch
if (refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE) {
git.log.addRange(command.getOldId, command.getNewId).call.asScala.foreach {
commit =>
closeIssuesFromMessage(commit.getFullMessage, pusher, owner, repository)
}
}
}
// call web hook
getWebHookURLs(owner, repository) match {
case webHookURLs if(webHookURLs.nonEmpty) =>
@@ -218,21 +180,14 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
private def updatePullRequests(branch: String) =
getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq =>
if(getRepository(pullreq.userName, pullreq.repositoryName, baseUrl).isDefined){
using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName)),
Git.open(Directory.getRepositoryDir(pullreq.requestUserName, pullreq.requestRepositoryName))){ (oldGit, newGit) =>
oldGit.fetch
using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName))){ git =>
git.fetch
.setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/pull/${pullreq.issueId}/head").setForceUpdate(true))
.call
val commitIdTo = oldGit.getRepository.resolve(s"refs/pull/${pullreq.issueId}/head").getName
val commitIdTo = git.getRepository.resolve(s"refs/pull/${pullreq.issueId}/head").getName
updateCommitIdTo(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo)
val commitIdFrom = JGitUtil.getForkedCommitId(oldGit, newGit,
pullreq.userName, pullreq.repositoryName, pullreq.branch,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch)
// TODO(tanacasino): commitIdFrom and commitIdTo should be updated by one query...
updateCommitIdFrom(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdFrom)
}
}
}

View File

@@ -1,16 +1,15 @@
package servlet
import javax.servlet.http.{HttpSessionEvent, HttpSessionListener}
import org.apache.commons.io.FileUtils
import util.Directory._
import app.FileUploadControllerBase
/**
* Removes session associated temporary files when session is destroyed.
*/
class SessionCleanupListener extends HttpSessionListener {
class SessionCleanupListener extends HttpSessionListener with FileUploadControllerBase {
def sessionCreated(se: HttpSessionEvent): Unit = {}
def sessionDestroyed(se: HttpSessionEvent): Unit = FileUtils.deleteDirectory(getTemporaryDir(se.getSession.getId))
def sessionDestroyed(se: HttpSessionEvent): Unit = removeTemporaryFiles()(se.getSession)
}

View File

@@ -1,132 +0,0 @@
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")){
val hook = new CommitLogHook(owner, repoName, user, baseUrl)
receive.setPreReceiveHook(hook)
receive.setPostReceiveHook(hook)
}
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

@@ -1,62 +0,0 @@
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

@@ -1,23 +0,0 @@
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

@@ -1,70 +0,0 @@
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)
logger.info("SSH Server is stopped.")
}
}
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 {
private val logger = LoggerFactory.getLogger(classOf[SshServerListener])
override def contextInitialized(sce: ServletContextEvent): Unit = {
val settings = loadSystemSettings()
if(settings.ssh){
settings.baseUrl match {
case None =>
logger.error("Could not start SshServer because the baseUrl is not configured.")
case Some(baseUrl) =>
SshServer.start(sce.getServletContext,
settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), baseUrl)
}
}
}
override def contextDestroyed(sce: ServletContextEvent): Unit = {
if(loadSystemSettings().ssh){
SshServer.stop()
}
}
}

View File

@@ -1,36 +0,0 @@
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): Option[String] = str2PublicKey(key) match {
case Some(publicKey) => Some(KeyUtils.getFingerPrint(publicKey))
case None => None
}
}

View File

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

View File

@@ -16,7 +16,7 @@ object ControlUtil {
def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B =
try f(resource) finally {
if(resource != null){
ignoring(classOf[Throwable]) {
allCatch {
resource.close()
}
}
@@ -37,4 +37,15 @@ object ControlUtil {
def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T =
try f(treeWalk) finally treeWalk.release()
// def withTmpRefSpec[T](ref: RefSpec, git: Git)(f: RefSpec => T): T = {
// try {
// f(ref)
// } finally {
// val refUpdate = git.getRepository.updateRef(ref.getDestination)
// refUpdate.setForceUpdate(true)
// refUpdate.delete()
// }
// }
}

View File

@@ -29,10 +29,24 @@ object Directory {
}).getAbsolutePath
val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
val RepositoryHome = s"${GitBucketHome}/repositories"
val DatabaseHome = s"${GitBucketHome}/data"
/**
* Repository names of the specified user.
*/
def getRepositories(owner: String): List[String] =
defining(new File(s"${RepositoryHome}/${owner}")){ dir =>
if(dir.exists){
dir.listFiles.filter { file =>
file.isDirectory && !file.getName.endsWith(".wiki.git")
}.map(_.getName.replaceFirst("\\.git$", "")).toList
} else {
Nil
}
}
/**
* Substance directory of the repository.
@@ -40,23 +54,11 @@ object Directory {
def getRepositoryDir(owner: String, repository: String): File =
new File(s"${RepositoryHome}/${owner}/${repository}.git")
/**
* Directory for files which are attached to issue.
*/
def getAttachedDir(owner: String, repository: String): File =
new File(s"${RepositoryHome}/${owner}/${repository}/issues")
/**
* Directory for uploaded files by the specified user.
*/
def getUserUploadDir(userName: String): File = new File(s"${GitBucketHome}/data/${userName}/files")
/**
* Root of temporary directories for the upload file.
*/
def getTemporaryDir(sessionId: String): File =
new File(s"${GitBucketHome}/tmp/_upload/${sessionId}")
/**
* Root of temporary directories for the specified repository.
*/

View File

@@ -4,10 +4,9 @@ import org.apache.commons.io.FileUtils
import java.net.URLConnection
import java.io.File
import util.ControlUtil._
import scala.util.Random
object FileUtil {
def getMimeType(name: String): String =
defining(URLConnection.getFileNameMap()){ fileNameMap =>
fileNameMap.getContentTypeFor(name) match {
@@ -27,12 +26,32 @@ object FileUtil {
}
def isImage(name: String): Boolean = getMimeType(name).startsWith("image/")
def isLarge(size: Long): Boolean = (size > 1024 * 1000)
def isText(content: Array[Byte]): Boolean = !content.contains(0)
def generateFileId: String = System.currentTimeMillis + Random.alphanumeric.take(10).mkString
// def createZipFile(dest: File, dir: File): Unit = {
// def addDirectoryToZip(out: ZipArchiveOutputStream, dir: File, path: String): Unit = {
// dir.listFiles.map { file =>
// if(file.isFile){
// out.putArchiveEntry(new ZipArchiveEntry(path + "/" + file.getName))
// out.write(FileUtils.readFileToByteArray(file))
// out.closeArchiveEntry
// } else if(file.isDirectory){
// addDirectoryToZip(out, file, path + "/" + file.getName)
// }
// }
// }
//
// using(new ZipArchiveOutputStream(dest)){ out =>
// addDirectoryToZip(out, dir, dir.getName)
// }
// }
def getFileName(path: String): String = defining(path.lastIndexOf('/')){ i =>
if(i >= 0) path.substring(i + 1) else path
}
def getExtension(name: String): String =
name.lastIndexOf('.') match {

View File

@@ -11,20 +11,17 @@ 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.{ConfigInvalidException, MissingObjectException}
import org.eclipse.jgit.errors.MissingObjectException
import java.util.Date
import org.eclipse.jgit.api.errors.NoHeadException
import service.RepositoryService
import org.eclipse.jgit.dircache.DirCacheEntry
import org.slf4j.LoggerFactory
/**
* Provides complex JGit operations.
*/
object JGitUtil {
private val logger = LoggerFactory.getLogger(JGitUtil.getClass)
/**
* The repository data.
*
@@ -48,10 +45,9 @@ 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, linkUrl: Option[String])
committer: String, mailAddress: String)
/**
* The commit data.
@@ -76,7 +72,11 @@ object JGitUtil {
rev.getFullMessage,
rev.getParents().map(_.name).toList)
val summary = getSummaryMessage(fullMessage, shortMessage)
val summary = defining(fullMessage.trim.indexOf("\n")){ i =>
defining(if(i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage){ firstLine =>
if(firstLine.length > shortMessage.length) shortMessage else firstLine
}
}
val description = defining(fullMessage.trim.indexOf("\n")){ i =>
if(i >= 0){
@@ -92,9 +92,8 @@ object JGitUtil {
*
* @param viewType "image", "large" or "other"
* @param content the string content
* @param charset the character encoding
*/
case class ContentInfo(viewType: String, content: Option[String], charset: Option[String])
case class ContentInfo(viewType: String, content: Option[String])
/**
* The tag data.
@@ -105,15 +104,6 @@ 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.
*
@@ -138,7 +128,7 @@ object JGitUtil {
using(Git.open(getRepositoryDir(owner, repository))){ git =>
try {
// get commit count
val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10001).sum
val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(1000).sum
RepositoryInfo(
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git",
@@ -162,7 +152,7 @@ object JGitUtil {
}
}
}
/**
* Returns the file list of the specified path.
*
@@ -172,7 +162,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, Option[String])]
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String)]
using(new RevWalk(git.getRepository)){ revWalk =>
val objectId = git.getRepository.resolve(revision)
@@ -205,30 +195,22 @@ object JGitUtil {
})
}
while (treeWalk.next()) {
// 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))
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString))
}
}
}
val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision)
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)
}
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)
}.sortWith { (file1, file2) =>
(file1.isDirectory, file2.isDirectory) match {
case (true , false) => true
@@ -237,18 +219,7 @@ 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.
*
@@ -354,6 +325,27 @@ 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.
*/
@@ -385,7 +377,7 @@ object JGitUtil {
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None)
} else {
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None,
JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray))
JGitUtil.getContent(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray))
}))
}
(buffer.toList, None)
@@ -408,8 +400,8 @@ object JGitUtil {
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None)
} else {
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray))
JGitUtil.getContent(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
JGitUtil.getContent(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray))
}
}.toList
}
@@ -481,7 +473,7 @@ object JGitUtil {
}
def createNewCommit(git: Git, inserter: ObjectInserter, headId: AnyObjectId, treeId: AnyObjectId,
fullName: String, mailAddress: String, message: String): ObjectId = {
fullName: String, mailAddress: String, message: String): String = {
val newCommit = new CommitBuilder()
newCommit.setCommitter(new PersonIdent(fullName, mailAddress))
newCommit.setAuthor(new PersonIdent(fullName, mailAddress))
@@ -499,134 +491,7 @@ object JGitUtil {
refUpdate.setNewObjectId(newHeadId)
refUpdate.update()
newHeadId
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)
}
}
def getContentInfo(git: Git, path: String, objectId: ObjectId): ContentInfo = {
// 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
if(viewer == "other"){
if(bytes.isDefined && FileUtil.isText(bytes.get)){
// text
ContentInfo("text", Some(StringUtil.convertFromByteArray(bytes.get)), Some(StringUtil.detectEncoding(bytes.get)))
} else {
// binary
ContentInfo("binary", None, None)
}
} else {
// image or large
ContentInfo(viewer, None, None)
}
}
/**
* 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
}
/**
* Returns all commit id in the specified repository.
*/
def getAllCommitIds(git: Git): Seq[String] = if(isEmpty(git)) {
Nil
} else {
val existIds = new scala.collection.mutable.ListBuffer[String]()
val i = git.log.all.call.iterator
while(i.hasNext){
existIds += i.next.name
}
existIds.toSeq
}
def processTree(git: Git, id: ObjectId)(f: (String, CanonicalTreeParser) => Unit) = {
using(new RevWalk(git.getRepository)){ revWalk =>
using(new TreeWalk(git.getRepository)){ treeWalk =>
val index = treeWalk.addTree(revWalk.parseTree(id))
treeWalk.setRecursive(true)
while(treeWalk.next){
f(treeWalk.getPathString, treeWalk.getTree(index, classOf[CanonicalTreeParser]))
}
}
}
}
/**
* Returns the identifier of the root commit (or latest merge commit) of the specified branch.
*/
def getForkedCommitId(oldGit: Git, newGit: Git,
userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): String =
defining(getAllCommitIds(oldGit)){ existIds =>
getCommitLogs(newGit, requestBranch, true) { commit =>
existIds.contains(commit.name) && getBranchesOfCommit(oldGit, commit.getName).contains(branch)
}.head.id
}
}

View File

@@ -49,7 +49,7 @@ object LDAPUtil {
){ conn =>
findMailAddress(conn, userDN, ldapSettings.mailAttribute) match {
case Some(mailAddress) => Right(LDAPUserInfo(
userName = getUserNameFromMailAddress(userName),
userName = userName,
fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute =>
findFullName(conn, userDN, fullNameAttribute)
}.getOrElse(userName),
@@ -59,13 +59,6 @@ object LDAPUtil {
}
}
private def getUserNameFromMailAddress(userName: String): String = {
(userName.indexOf('@') match {
case i if i >= 0 => userName.substring(0, i)
case i => userName
}).replaceAll("[^a-zA-Z0-9\\-_.]", "").replaceAll("^[_\\-]", "")
}
private def bind[A](host: String, port: Int, dn: String, password: String, tls: Boolean, keystore: String, error: String)
(f: LDAPConnection => Either[String, A]): Either[String, A] = {
if (tls) {

View File

@@ -31,7 +31,7 @@ object StringUtil {
/**
* Make string from byte array. Character encoding is detected automatically by [[util.StringUtil.detectEncoding]].
* And if given bytes contains UTF-8 BOM, it's removed from returned string.
* And if given bytes contains UTF-8 BOM, it's removed from returned string..
*/
def convertFromByteArray(content: Array[Byte]): String =
IOUtils.toString(new BOMInputStream(new java.io.ByteArrayInputStream(content)), detectEncoding(content))
@@ -47,21 +47,12 @@ object StringUtil {
}
/**
* Extract issue id like ```#issueId``` from the given message.
* Extract issue id like ````#issueId``` from the given message.
*
*@param message the message which may contains issue id
* @return the iterator of issue id
*/
def extractIssueId(message: String): Iterator[String] =
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(message).matchData.map(_.group(2))
/**
* Extract close issue id like ```close #issueId ``` from the given message.
*
* @param message the message which may contains close command
* @return the iterator of issue id
*/
def extractCloseId(message: String): Iterator[String] =
"(?i)(?<!\\w)(?:fix(?:e[sd])?|resolve[sd]?|close[sd]?)\\s+#(\\d+)(?!\\w)".r.findAllIn(message).matchData.map(_.group(1))
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(message).matchData.map { matchData => matchData.group(2) }
}

View File

@@ -16,8 +16,8 @@ trait AvatarImageProvider { self: RequestCache =>
val src = if(mailAddress.isEmpty){
// by user name
getAccountByUserName(userName).map { account =>
if(account.image.isEmpty && context.settings.gravatar){
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
if(account.image.isEmpty && getSystemSettings().gravatar){
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}"""
} else {
s"""${context.path}/${account.userName}/_avatar"""
}
@@ -27,14 +27,14 @@ trait AvatarImageProvider { self: RequestCache =>
} else {
// by mail address
getAccountByMailAddress(mailAddress).map { account =>
if(account.image.isEmpty && context.settings.gravatar){
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
if(account.image.isEmpty && getSystemSettings().gravatar){
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}"""
} else {
s"""${context.path}/${account.userName}/_avatar"""
}
} getOrElse {
if(context.settings.gravatar){
s"""https://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
if(getSystemSettings().gravatar){
s"""https://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}"""
} else {
s"""${context.path}/_unknown/_avatar"""
}
@@ -42,9 +42,9 @@ trait AvatarImageProvider { self: RequestCache =>
}
if(tooltip){
Html(s"""<img src="${src}" class="${if(size > 20){"avatar"} else {"avatar-mini"}}" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title="${userName}"/>""")
Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title="${userName}"/>""")
} else {
Html(s"""<img src="${src}" class="${if(size > 20){"avatar"} else {"avatar-mini"}}" style="width: ${size}px; height: ${size}px;" />""")
Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" />""")
}
}

View File

@@ -45,7 +45,7 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
(text, text)
}
val url = repository.httpUrl.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/" + StringUtil.urlEncode(page)
val url = repository.url.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.httpUrl.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/_blob/" + url
repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/_blob/" + url
}
}

View File

@@ -15,11 +15,6 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
*/
def datetime(date: Date): String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date)
/**
* Format java.util.Date to "yyyy-MM-dd'T'hh:mm:ss'Z'".
*/
def datetimeRFC3339(date: Date): String = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'").format(date).replaceAll("(\\d\\d)(\\d\\d)$","$1:$2")
/**
* Format java.util.Date to "yyyy-MM-dd".
*/
@@ -32,14 +27,6 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
def plural(count: Int, singular: String, plural: String = ""): String =
if(count == 1) singular else if(plural.isEmpty) singular + "s" else plural
private[this] val renderersBySuffix: Seq[(String, (List[String], String, String, service.RepositoryService.RepositoryInfo, Boolean, Boolean, app.Context) => Html)] =
Seq(
".md" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => markdown(fileContent, repository, enableWikiLink, enableRefsLink)(context)),
".markdown" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => markdown(fileContent, repository, enableWikiLink, enableRefsLink)(context))
)
def renderableSuffixes: Seq[String] = renderersBySuffix.map(_._1)
/**
* Converts Markdown of Wiki pages to HTML.
*/
@@ -47,21 +34,6 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink))
def renderMarkup(filePath: List[String], fileContent: String, branch: String,
repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = {
val fileNameLower = filePath.reverse.head.toLowerCase
renderersBySuffix.find { case (suffix, _) => fileNameLower.endsWith(suffix) } match {
case Some((_, handler)) => handler(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context)
case None => Html(
s"<tt>${
fileContent.split("(\\r\\n)|\\n").map(xml.Utility.escape(_)).mkString("<br/>")
}</tt>"
)
}
}
/**
* Returns &lt;img&gt; which displays the avatar icon for the given user name.
* This method looks up Gravatar if avatar icon has not been configured in user settings.
@@ -163,45 +135,6 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
*/
def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime
/**
* Returns file type for AceEditor.
*/
def editorType(fileName: String): String = {
fileName.toLowerCase match {
case x if(x.endsWith(".bat")) => "batchfile"
case x if(x.endsWith(".java")) => "java"
case x if(x.endsWith(".scala")) => "scala"
case x if(x.endsWith(".js")) => "javascript"
case x if(x.endsWith(".css")) => "css"
case x if(x.endsWith(".md")) => "markdown"
case x if(x.endsWith(".html")) => "html"
case x if(x.endsWith(".xml")) => "xml"
case x if(x.endsWith(".c")) => "c_cpp"
case x if(x.endsWith(".cpp")) => "c_cpp"
case x if(x.endsWith(".coffee")) => "coffee"
case x if(x.endsWith(".ejs")) => "ejs"
case x if(x.endsWith(".hs")) => "haskell"
case x if(x.endsWith(".json")) => "json"
case x if(x.endsWith(".jsp")) => "jsp"
case x if(x.endsWith(".jsx")) => "jsx"
case x if(x.endsWith(".cl")) => "lisp"
case x if(x.endsWith(".clojure")) => "lisp"
case x if(x.endsWith(".lua")) => "lua"
case x if(x.endsWith(".php")) => "php"
case x if(x.endsWith(".py")) => "python"
case x if(x.endsWith(".rdoc")) => "rdoc"
case x if(x.endsWith(".rhtml")) => "rhtml"
case x if(x.endsWith(".ruby")) => "ruby"
case x if(x.endsWith(".sh")) => "sh"
case x if(x.endsWith(".sql")) => "sql"
case x if(x.endsWith(".tcl")) => "tcl"
case x if(x.endsWith(".vbs")) => "vbscript"
case x if(x.endsWith(".tcl")) => "tcl"
case x if(x.endsWith(".yml")) => "yaml"
case _ => "plain_text"
}
}
/**
* Implicit conversion to add mkHtml() to Seq[Html].
*/

View File

@@ -2,8 +2,5 @@
@import context._
@import view.helpers._
@main(account, groupNames, "activity"){
<div class="pull-right">
<a href="@path/@{account.userName}.atom"><img src="@assets/common/images/feed.png" alt="activities"></a>
</div>
@helper.html.activities(activities)
}

View File

@@ -1,62 +1,71 @@
@(account: model.Account, info: Option[Any])(implicit context: app.Context)
@(account: Option[model.Account], info: Option[Any])(implicit context: app.Context)
@import context._
@import view.helpers._
@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>
@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>
</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

@@ -1,140 +0,0 @@
@(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,5 +1,4 @@
@(account: model.Account, groupNames: List[String], active: String,
isGroupManager: Boolean = false)(body: Html)(implicit context: app.Context)
@(account: model.Account, groupNames: List[String], active: String)(body: Html)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(account.userName){
@@ -28,7 +27,7 @@
</div>
<div class="span8">
<ul class="nav nav-tabs" style="margin-bottom: 5px;">
<ul class="nav nav-tabs">
<li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
@if(account.isGroupAccount){
<li@if(active == "members"){ class="active"}><a href="@url(account.userName)?tab=members">Members</a></li>
@@ -42,13 +41,6 @@
</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], isGroupManager: Boolean)(implicit context: app.Context)
@(account: model.Account, members: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@main(account, Nil, "members", isGroupManager){
@main(account, Nil, "members"){
@if(members.isEmpty){
No members
} else {

View File

@@ -1,14 +0,0 @@
@(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,48 +0,0 @@
@()(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,32 +1,25 @@
@(account: model.Account, groupNames: List[String],
repositories: List[service.RepositoryService.RepositoryInfo],
isGroupManager: Boolean)(implicit context: app.Context)
@(account: model.Account, groupNames: List[String], repositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
@import context._
@import view.helpers._
@main(account, groupNames, "repositories", isGroupManager){
@main(account, groupNames, "repositories"){
@if(repositories.isEmpty){
No repositories
} else {
@repositories.map { repository =>
<div class="block">
<div class="repository-icon">
@helper.html.repositoryicon(repository, true)
</div>
<div class="repository-content">
<div class="block-header">
<a href="@url(repository)">@repository.name</a>
@if(repository.repository.isPrivate){
<i class="icon-lock"></i>
}
</div>
@if(repository.repository.originUserName.isDefined){
<div class="small muted">forked from <a href="@path/@repository.repository.parentUserName/@repository.repository.parentRepositoryName">@repository.repository.parentUserName/@repository.repository.parentRepositoryName</a></div>
<div class="block-header">
<a href="@url(repository)">@repository.name</a>
@if(repository.repository.isPrivate){
<i class="icon-lock"></i>
}
@if(repository.repository.description.isDefined){
<div>@repository.repository.description</div>
}
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
</div>
@if(repository.repository.originUserName.isDefined){
<div class="small muted">forked from <a href="@path/@repository.repository.parentUserName/@repository.repository.parentRepositoryName">@repository.repository.parentUserName/@repository.repository.parentRepositoryName</a></div>
}
@if(repository.repository.description.isDefined){
<div>@repository.repository.description</div>
}
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
</div>
}
}

View File

@@ -1,45 +0,0 @@
@(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).getOrElse("Key is invalid."))
<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 @@
@(info: Option[Any])(implicit context: app.Context)
@(settings: service.SystemSettingsService.SystemSettings, info: Option[Any])(implicit context: app.Context)
@import context._
@import util.Directory._
@import view.helpers._
@@ -22,10 +22,9 @@
<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 class="muted">
<p>
The base URL is used for redirect, notification email, git repository URL box and more.
If the base URL is empty, GitBucket generates URL from request information.
You can use this property to adjust URL difference between the reverse proxy and GitBucket.
@@ -57,29 +56,6 @@
</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>
@@ -230,10 +206,6 @@
}
<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[model.GroupMember])(implicit context: app.Context)
@(account: Option[model.Account], members: List[String])(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="span5">
<div class="span7">
<fieldset>
<label for="groupName" class="strong">Group name</label>
<div>
@@ -24,23 +24,29 @@
<div>
<span id="error-url" class="error"></span>
</div>
<input type="text" name="url" id="url" value="@account.map(_.url)"/>
<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">
<div class="span5">
<fieldset>
<label class="strong">Members</label>
<ul id="member-list" class="collaborator">
<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>
@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(",")"/>
<input type="hidden" id="memberNames" name="memberNames" value="@members.mkString(",")"/>
<div>
<span class="error" id="error-members"></span>
<span class="error" id="error-memberName"></span>
</div>
</fieldset>
</div>
@@ -54,10 +60,6 @@
}
<script>
$(function(){
$('input[type=submit]').click(function(){
updateMembers();
});
$('#addMember').click(function(){
$('#error-memberName').text('');
var userName = $('#memberName').val();
@@ -68,7 +70,7 @@ $(function(){
}
// check duplication
var exists = $('#member-list li').filter(function(){
var exists = $('#members li').filter(function(){
return $(this).data('name') == userName;
}).length > 0;
if(exists){
@@ -81,7 +83,19 @@ $(function(){
'userName': userName
}, function(data, status){
if(data == 'true'){
addMemberHTML(userName, false);
// 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);
} else {
$('#error-memberName').text('User does not exist.');
}
@@ -89,47 +103,20 @@ $(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

@@ -1,13 +1,9 @@
@(active: String = "")(implicit context: app.Context)
@import context._
@import view.helpers._
<ul class="nav nav-tabs">
<li@if(active == ""){ class="active"}><a href="@path/">News Feed</a></li>
@if(loginAccount.isDefined){
<li@if(active == "pulls" ){ class="active"}><a href="@path/dashboard/pulls">Pull Requests</a></li>
<li@if(active == "issues"){ class="active"}><a href="@path/dashboard/issues/repos">Issues</a></li>
}
@if(active == ""){
<li class="pull-right"><a href="@path/activities.atom"><img src="@assets/common/images/feed.png" alt="activities"></a></li>
}
</ul>

View File

@@ -6,12 +6,20 @@
<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 count"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
<span class="add-on"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
</div>
</div>
}
<div class="head">
@helper.html.repositoryicon(repository, true)
@if(repository.repository.isPrivate){
<img src="@assets/common/images/repo_private_lg.png"/>
} else {
@if(repository.repository.originUserName.isDefined){
<img src="@assets/common/images/repo_fork_lg.png"/>
} else {
<img src="@assets/common/images/repo_public_lg.png"/>
}
}
<a href="@url(repository.owner)">@repository.owner</a> / <a href="@url(repository)" class="strong">@repository.name</a>
@defining(repository.repository){ x =>
@@ -22,9 +30,6 @@
}
}
</div>
@repository.repository.description.map { description =>
<p>@description</p>
}
<table class="global-nav box-header">
<tr>
<th class="box-header@if(active=="code"){ active}">
@@ -48,7 +53,7 @@
<th class="box-header@if(active=="network"){ active}">
<a href="@url(repository)/network/members">Network</a>
</th>
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || loginAccount.get.userName == repository.owner)){
<th class="box-header@if(active=="settings"){ active}">
<a href="@url(repository)/settings">Settings</a>
</th>

View File

@@ -1,27 +0,0 @@
@(owner: String, repository: String)(textarea: Html)(implicit context: app.Context)
@import context._
<div class="muted attachable">
@textarea
<div>Attach images by dragging &amp; dropping, or selecting them.</div>
</div>
@defining("(id=\")([\\w\\-]*)(\")".r.findFirstMatchIn(textarea.body).map(_.group(2))){ textareaId =>
<script>
$(function(){
$('#@textareaId').next('div').dropzone({
url: '@path/upload/image/@owner/@repository',
maxFilesize: 10,
acceptedFiles: 'image/*',
dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, or JPG.',
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your images...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
success: function(file, id) {
var images = '\n![' + file.name.split('.')[0] + '](@baseUrl/@owner/@repository/_attached/' + id + ')';
$('#@textareaId').val($('#@textareaId').val() + images);
$(file.previewElement).prevAll('div.dz-preview').addBack().remove();
}
});
// Adjust attachment area width
$('div.attachable div').css('width', ($('div.attachable textarea').width() + 8) + 'px');
});
</script>
}

View File

@@ -1,5 +1,5 @@
@(id: String, value: String, prepend: Boolean = false)(html: Html)
<div class="input-append@if(prepend){ input-prepend}">
@(id: String, value: String)(html: Html)
<div class="input-append">
@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,30 +0,0 @@
@(activities: List[model.Activity])(implicit context: app.Context)<?xml version="1.0" encoding="UTF-8"?>
@import context._
@import view.helpers._
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-US">
@header(activities)
@activities.map { activity => @item(activity) }
</feed>
@header(activities: List[model.Activity]) = {<id>tag:@context.host,2013:gitbucket</id>
<title>Gitbucket's activities</title>
<link type="application/atom+xml" rel="self" href="@context.baseUrl/activities.atom"/>
<author>
<name>Gitbucket</name>
<uri>@context.baseUrl</uri>
</author>
<updated>@datetimeRFC3339(activities.map(_.activityDate).max)</updated>}
@item(activity: model.Activity) = {
<entry>
<id>tag:@context.host,@date(activity.activityDate):activity:@activity.activityId</id>
<published>@datetimeRFC3339(activity.activityDate)</published>
<updated>@datetimeRFC3339(activity.activityDate)</updated>
<link type="text/html" rel="alternate" href="@context.baseUrl/@activity.userName/@activity.repositoryName" />
<title type="html">@activity.activityType</title>
<author>
<name>@activity.activityUserName</name>
<uri>@url(activity.activityUserName)</uri>
</author>
<content type="html">@activityMessage(activity.message)</content>
</entry>}

View File

@@ -6,18 +6,16 @@
<ul class="nav nav-tabs">
<li class="active"><a href="#tab1" data-toggle="tab">Write</a></li>
<li><a href="#tab2" data-toggle="tab" id="preview">Preview</a></li>
@*
<li class="pull-right">
<a href="http://daringfireball.net/projects/markdown/syntax" target="_blank">Markdown Syntax Guide</a>
</li>
*@
</ul>
<div class="tab-content">
<div class="tab-pane active" id="tab1">
<span id="error-content" class="error"></span>
@textarea = {
<textarea id="content" name="content"@if(style.nonEmpty){ style="@style"} placeholder="@placeholder">@content</textarea>
}
@if(enableWikiLink){
@textarea
} else {
@helper.html.attached(repository.owner, repository.name)(textarea)
}
<textarea id="content" name="content"@if(style.nonEmpty){ style="@style"} placeholder="@placeholder">@content</textarea>
</div>
<div class="tab-pane" id="tab2">
<div class="markdown-body" id="preview-area">

View File

@@ -1,12 +0,0 @@
@(repository: service.RepositoryService.RepositoryInfo, large: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@if(repository.repository.isPrivate){
<img src="@assets/common/images/repo_private@{if(large){"_lg"}}.png"/>
} else {
@if(repository.repository.originUserName.isDefined){
<img src="@assets/common/images/repo_fork@{if(large){"_lg"}}.png"/>
} else {
<img src="@assets/common/images/repo_public@{if(large){"_lg"}}.png"/>
}
}

View File

@@ -4,7 +4,9 @@
@if(account.nonEmpty && account.get.image.nonEmpty){
<img src="@path/@account.get.userName/_avatar" style="with: 120px; height: 120px;"/>
} else {
<div id="clickable">Upload Image</div>
<div id="clickable">
<a href="https://www.gravatar.com/" target="_blank">Gravatar</a> is used
</div>
}
</div>
@if(account.nonEmpty && account.get.image.nonEmpty){
@@ -19,6 +21,7 @@ $(function(){
var dropzone = new Dropzone('div#clickable', {
url: '@path/upload/image',
previewsContainer: 'div#avatar',
paramName: 'file',
parallelUploads: 1,
thumbnailWidth: 120,
thumbnailHeight: 120

View File

@@ -1,5 +1,6 @@
@(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._
@@ -11,7 +12,7 @@
</div>
<div class="span4">
@if(loginAccount.isEmpty){
@signinform(settings)
@signinform(systemSettings)
} else {
<table class="table table-bordered">
<tr>
@@ -30,7 +31,15 @@
@userRepositories.map { repository =>
<tr>
<td>
@helper.html.repositoryicon(repository, false)
@if(repository.repository.isPrivate){
<img src="@assets/common/images/repo_private.png"/>
} else {
@if(repository.repository.originUserName.isDefined){
<img src="@assets/common/images/repo_fork.png"/>
} else {
<img src="@assets/common/images/repo_public.png"/>
}
}
@if(repository.owner == loginAccount.get.userName){
<a href="@url(repository)"><span class="strong">@repository.name</span></a>
} else {
@@ -49,7 +58,7 @@
Recent updated repositories
</th>
</tr>
@if(recentRepositories.isEmpty){2
@if(recentRepositories.isEmpty){
<tr>
<td>No repositories</td>
</tr>
@@ -57,7 +66,15 @@
@recentRepositories.map { repository =>
<tr>
<td>
@helper.html.repositoryicon(repository, false)
@if(repository.repository.isPrivate){
<img src="@assets/common/images/repo_private.png"/>
} else {
@if(repository.repository.originUserName.isDefined){
<img src="@assets/common/images/repo_fork.png"/>
} else {
<img src="@assets/common/images/repo_public.png"/>
}
}
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
</td>
</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.filter(_.closedDate.isEmpty).map { milestone =>
@milestones.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

@@ -1,9 +1,7 @@
@(content: String, commentId: Int, owner: String, repository: String)(implicit context: app.Context)
@import context._
<span id="error-edit-content-@commentId" class="error"></span>
@helper.html.attached(owner, repository){
<textarea style="width: 680px; height: 100px;" id="edit-content-@commentId">@content</textarea>
}
<textarea style="width: 680px; height: 100px;" id="edit-content-@commentId">@content</textarea>
<div>
<input type="button" id="update-comment-@commentId" class="btn btn-small" value="Update Comment"/>
<input type="button" id="cancel-comment-@commentId" class="btn btn-small btn-danger pull-right" value="Cancel"/>

View File

@@ -2,9 +2,7 @@
@import context._
<span id="error-edit-title" class="error"></span>
<input type="text" style="width: 680px;" id="edit-title" value="@title"/>
@helper.html.attached(owner, repository){
<textarea style="width: 680px; height: 100px; max-height: 300px;" id="edit-content">@content.getOrElse("")</textarea>
}
<textarea style="width: 680px; height: 100px; max-height: 300px;" id="edit-content">@content.getOrElse("")</textarea>
<div>
<input type="button" id="update" class="btn btn-small" value="Update Issue"/>
<input type="button" id="cancel" class="btn btn-small btn-danger pull-right" value="Cancel"/>

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.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
@milestones.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,11 +54,7 @@
}
@if(loginAccount.isDefined){
<a href="@url(loginAccount.get.userName)" class="username menu">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</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="@path/new" class="menu" data-toggle="tooltip" data-placement="bottom" title="Create a new repo"><i class="icon-plus"></i></a>
<a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user"></i></a>
@if(loginAccount.get.isAdmin){
<a href="@path/admin/users" class="menu" data-toggle="tooltip" data-placement="bottom" title="Administration"><i class="icon-wrench"></i></a>

View File

@@ -1,7 +1,7 @@
@(groupNames: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Create a New Repository"){
@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

@@ -2,8 +2,7 @@
repository: service.RepositoryService.RepositoryInfo,
pathList: List[String],
content: util.JGitUtil.ContentInfo,
latestCommit: util.JGitUtil.CommitInfo,
hasWritePermission: Boolean)(implicit context: app.Context)
latestCommit: util.JGitUtil.CommitInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@@ -30,27 +29,15 @@
<a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a>
</div>
<div class="btn-group pull-right">
@if(hasWritePermission){
<a class="btn" href="@url(repository)/edit/@encodeRefName(branch)/@pathList.mkString("/")">Edit</a>
}
<a class="btn" href="?raw=true">Raw</a>
<a class="btn" href="@url(repository)/commits/@encodeRefName(branch)/@pathList.mkString("/")">History</a>
@if(hasWritePermission){
<a class="btn btn-danger" href="@url(repository)/remove/@encodeRefName(branch)/@pathList.mkString("/")">Delete</a>
}
</div>
<a class="btn btn-mini" href="?raw=true">Raw</a>
<a class="btn btn-mini" href="@url(repository)/commits/@encodeRefName(branch)/@pathList.mkString("/")">History</a>
</div>
</th>
</tr>
<tr>
<td>
@if(content.viewType == "text"){
@defining(pathList.reverse.head) { file =>
@if(renderableSuffixes.find(suffix => file.toLowerCase.endsWith(suffix))) {
@renderMarkup(pathList, content.content.get, branch, repository, false, false)
} else {
<pre class="prettyprint linenums blob">@content.content.get</pre>
}
}
<pre class="prettyprint linenums blob">@content.content.get</pre>
}
@if(content.viewType == "image"){
<img src="?raw=true"/>

View File

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

View File

@@ -1,110 +0,0 @@
@(branch: String,
repository: service.RepositoryService.RepositoryInfo,
pathList: List[String],
fileName: String,
content: util.JGitUtil.ContentInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(s"Deleting ${path} at ${fileName} - ${repository.owner}/${repository.name}", Some(repository)) {
@html.header("code", repository)
@tab(branch, repository, "files")
<form method="POST" action="@url(repository)/remove" validate="true">
<div class="head">
<a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> /
@pathList.zipWithIndex.map { case (section, i) =>
<a href="@url(repository)/tree/@encodeRefName(branch)/@pathList.take(i + 1).mkString("/")">@section</a> /
}
@fileName
<input type="hidden" name="fileName" id="fileName" value="@fileName"/>
<input type="hidden" name="branch" id="branch" value="@branch"/>
<input type="hidden" name="path" id="path" value="@pathList.mkString("/")"/>
</div>
<table class="table table-bordered">
<th style="font-weight: normal;" class="box-header">
@fileName
<div class="pull-right align-right">
<a href="@url(repository)/blob/@branch/@{(pathList ::: List(fileName)).mkString("/")}" class="btn btn-small">View</a>
</div>
</th>
<tr>
<td>
<div id="diffText"></div>
<textarea id="newText" style="display: none;"></textarea>
<textarea id="oldText" style="display: none;">@content.content</textarea>
</td>
</tr>
</table>
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
<div class="box issue-comment-box">
<div class="box-content">
<div>
<strong>Commit changes</strong>
</div>
<div>
<input type="text" name="message" style="width: 98%;"/>
</div>
<div style="text-align: right;">
<a href="@url(repository)/blob/@encodeRefName(branch)/@pathList.mkString("/")" class="btn btn-danger">Cancel</a>
<input type="submit" id="commit" class="btn btn-success" value="Commit changes"/>
</div>
</div>
</div>
</form>
}
<script type="text/javascript" src="@assets/jsdifflib/difflib.js"></script>
<script type="text/javascript" src="@assets/jsdifflib/diffview.js"></script>
<link href="@assets/jsdifflib/diffview.css" type="text/css" rel="stylesheet" />
<style type="text/css">
table.inlinediff {
width: 100%;
}
table.inlinediff thead {
display: none;
}
td.insert, td.equal, td.delete {
width: 100%;
}
</style>
<script>
function diffUsingJS(oldTextId, newTextId, outputId) {
// get the baseText and newText values from the two textboxes, and split them into lines
var oldText = document.getElementById(oldTextId).value;
if(oldText == ''){
var oldLines = [];
} else {
var oldLines = difflib.stringAsLines(oldText);
}
var newText = document.getElementById(newTextId).value
if(newText == ''){
var newLines = [];
} else {
var newLines = difflib.stringAsLines(newText);
}
// create a SequenceMatcher instance that diffs the two sets of lines
var sm = new difflib.SequenceMatcher(oldLines, newLines);
// get the opcodes from the SequenceMatcher instance
// opcodes is a list of 3-tuples describing what changes should be made to the base text
// in order to yield the new text
var opcodes = sm.get_opcodes();
var diffoutputdiv = document.getElementById(outputId);
while (diffoutputdiv.firstChild) diffoutputdiv.removeChild(diffoutputdiv.firstChild);
// build the diff view and add it to the current DOM
diffoutputdiv.appendChild(diffview.buildView({
baseTextLines: oldLines,
newTextLines: newLines,
opcodes: opcodes,
contextSize: 4,
viewType: 1
}));
}
$(function(){
diffUsingJS('oldText', 'newText', 'diffText');
});
</script>

View File

@@ -1,107 +0,0 @@
@(branch: String,
repository: service.RepositoryService.RepositoryInfo,
pathList: List[String],
fileName: Option[String],
content: util.JGitUtil.ContentInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(if(fileName.isEmpty) "New File" else s"Editing ${path} at ${fileName} - ${repository.owner}/${repository.name}", Some(repository)) {
@html.header("code", repository)
@tab(branch, repository, "files")
<form method="POST" action="@url(repository)/@if(fileName.isEmpty){create}else{update}" validate="true">
<span class="error" id="error-newFileName"></span>
<div class="head">
<a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> /
@pathList.zipWithIndex.map { case (section, i) =>
<a href="@url(repository)/tree/@encodeRefName(branch)/@pathList.take(i + 1).mkString("/")">@section</a> /
}
<input type="text" name="newFileName" id="newFileName" placeholder="Name your file..." value="@fileName"/>
<input type="hidden" name="oldFileName" id="oldFileName" value="@fileName"/>
<input type="hidden" name="branch" id="branch" value="@branch"/>
<input type="hidden" name="path" id="path" value="@pathList.mkString("/")"/>
</div>
<table class="table table-bordered">
<tr>
<th>
<div class="pull-right">
<select id="wrap" class="input-medium" style="margin-bottom: 0px;">
<optgroup label="Line Wrap Mode">
<option value="false">No wrap</option>
<option value="true">Soft wrap</option>
</optgroup>
</select>
</div>
</th>
</tr>
<tr>
<td>
<div id="editor" style="width: 100%; height: 600px;"></div>
</td>
</tr>
</table>
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
<div class="box issue-comment-box">
<div class="box-content">
<div>
<strong>Commit changes</strong>
</div>
<div>
<input type="text" name="message" style="width: 98%;"/>
</div>
<div style="text-align: right;">
@if(fileName.isEmpty){
<a href="@url(repository)/tree/@encodeRefName(branch)/@{pathList.mkString("/")}" class="btn btn-danger">Cancel</a>
} else {
<a href="@url(repository)/blob/@encodeRefName(branch)/@{(pathList ++ Seq(fileName.get)).mkString("/")}" class="btn btn-danger">Cancel</a>
}
<input type="submit" id="commit" class="btn btn-success" value="Commit changes" disabled="true"/>
<input type="hidden" id="charset" name="charset" value="@content.charset"/>
<input type="hidden" id="content" name="content" value=""/>
<input type="hidden" id="initial" value="@content.content"/>
</div>
</div>
</div>
</form>
}
<script src="@assets/ace/ace.js" type="text/javascript" charset="utf-8"></script>
<script>
$(function(){
$('#editor').text($('#initial').val());
var editor = ace.edit("editor");
editor.setTheme("ace/theme/monokai");
//editor.getSession().setUseWrapMode(false);
@if(fileName.isDefined){
editor.getSession().setMode("ace/mode/@editorType(fileName.get)");
}
editor.on('change', function(){
updateCommitButtonStatus();
});
function updateCommitButtonStatus(){
if(editor.getValue() == $('#initial').val() && $('#newFileName').val() == $('#oldFileName').val()){
$('#commit').attr('disabled', true);
} else {
$('#commit').attr('disabled', false);
}
}
$('#wrap').change(function(){
console.log($('#wrap option:selected').val());
if($('#wrap option:selected').val() == 'true'){
editor.getSession().setUseWrapMode(true);
} else {
editor.getSession().setUseWrapMode(false);
}
});
$('#newFileName').watch(function(){
updateCommitButtonStatus();
});
$('#commit').click(function(){
$('#content').val(editor.getValue());
});
});
</script>

View File

@@ -3,8 +3,7 @@
pathList: List[String],
latestCommit: util.JGitUtil.CommitInfo,
files: List[util.JGitUtil.FileInfo],
readme: Option[(List[String], String)],
hasWritePermission: Boolean)(implicit context: app.Context)
readme: Option[(util.JGitUtil.FileInfo, String)])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@@ -13,16 +12,13 @@
<div class="head">
<div class="pull-right">
@defining(repository.commitCount){ commitCount =>
<a href="@url(repository)/commits/@encodeRefName(branch)">@if(commitCount > 10000){ 10000+ } else { @commitCount } @plural(commitCount, "commit")</a>&nbsp;
<a href="@url(repository)/commits/@encodeRefName(branch)">@if(commitCount > 1000){ @commitCount+ } else { @commitCount } @plural(commitCount, "commit")</a>&nbsp;
}
</div>
<a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> /
@pathList.zipWithIndex.map { case (section, i) =>
<a href="@url(repository)/tree/@encodeRefName(branch)/@pathList.take(i + 1).mkString("/")">@section</a> /
}
@if(hasWritePermission){
<a href="@url(repository)/new/@encodeRefName(branch)/@pathList.mkString("/")"><img src="@assets/common/images/newfile.png"/></a>
}
</div>
<div class="box">
<table class="table table-file-list" style="border: 1px solid silver;">
@@ -59,22 +55,14 @@
<tr>
<td width="16">
@if(file.isDirectory){
@if(file.linkUrl.isDefined){
<img src="@assets/common/images/folder_link.png"/>
} else {
<img src="@assets/common/images/folder.png"/>
}
<img src="@assets/common/images/folder.png"/>
} else {
<img src="@assets/common/images/file.png"/>
}
</td>
<td>
@if(file.isDirectory){
@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>
}
<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>
}
@@ -89,10 +77,10 @@
</table>
</div>
@readme.map { case(filePath, content) =>
@readme.map { case(file, content) =>
<div id="readme" class="box">
<div class="box-header">@filePath.reverse.head</div>
<div class="box-content markdown-body">@renderMarkup(filePath, content, branch, repository, false, false)</div>
<div class="box-header">@file.name</div>
<div class="box-content markdown-body">@markdown(content, repository, false, false)</div>
</div>
}
}

View File

@@ -1,26 +1,21 @@
@(repository: service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: app.Context)
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.header("code", repository)
@if(!hasWritePermission){
<h3>This is an empty repository</h3>
} else {
<h3 style="margin-top: 30px;">Create a new repository on the command line</h3>
<pre>
touch README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin @repository.httpUrl
git push -u origin master
</pre>
@html.header("code", repository)
<h3 style="margin-top: 30px;">Create a new repository on the command line</h3>
<pre>
touch README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin @repository.url
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.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 push -u origin master
</pre>
}

View File

@@ -22,15 +22,8 @@
<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.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>
@helper.html.copy("repository-url-copy", repository.url){
<input type="text" value="@repository.url" id="repository-url" readonly>
}
</li>
<li class="pull-right" style="margin-right: 8px;">
@@ -39,17 +32,3 @@
</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,10 +13,6 @@
<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

@@ -20,18 +20,11 @@
</fieldset>
<fieldset class="margin">
<label for="defaultBranch" class="strong">Default Branch:</label>
<select name="defaultBranch" id="defaultBranch"@if(repository.branchList.isEmpty){ disabled}>
@if(repository.branchList.isEmpty){
<option value="none" selected>No Branch</option>
} else {
@repository.branchList.map { branch =>
<option@if(branch==repository.repository.defaultBranch){ selected}>@branch</option>
}
<select name="defaultBranch" id="defaultBranch">
@repository.branchList.map { branch =>
<option value="@branch"@if(branch==repository.repository.defaultBranch){ selected}>@branch</option>
}
</select>
@if(repository.branchList.isEmpty){
<input type="hidden" name="defaultBranch" value="none"/>
}
<span class="error" id="error-defaultBranch"></span>
</fieldset>
<fieldset class="margin">

View File

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

View File

@@ -1,6 +1,4 @@
@(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,36 +1,15 @@
@(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">
@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>
@defining(repository.url.replaceFirst("\\.git$", ".wiki.git")){ repositoryUrl =>
@helper.html.copy("repository-url-copy", repositoryUrl){
<input type="text" value="@repositoryUrl" readonly id="repository-url">
}
<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,7 +18,6 @@
<listener-class>servlet.AutoUpdateListener</listener-class>
</listener>
<!-- ===================================================================== -->
<!-- Scalatra configuration -->
<!-- ===================================================================== -->
@@ -26,13 +25,6 @@
<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 -->
<!-- ===================================================================== -->

File diff suppressed because it is too large Load Diff

View File

@@ -1,360 +0,0 @@
/* ***** BEGIN LICENSE BLOCK *****
* Distributed under the BSD license:
*
* Copyright (c) 2012, Ajax.org B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of Ajax.org B.V. nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* ***** END LICENSE BLOCK ***** */
ace.define('ace/ext/beautify', ['require', 'exports', 'module' , 'ace/token_iterator', 'ace/ext/beautify/php_rules'], function(require, exports, module) {
var TokenIterator = require("ace/token_iterator").TokenIterator;
var phpTransform = require("./beautify/php_rules").transform;
exports.beautify = function(session) {
var iterator = new TokenIterator(session, 0, 0);
var token = iterator.getCurrentToken();
var context = session.$modeId.split("/").pop();
var code = phpTransform(iterator, context);
session.doc.setValue(code);
};
exports.commands = [{
name: "beautify",
exec: function(editor) {
exports.beautify(editor.session);
},
bindKey: "Ctrl-Shift-B"
}]
});
ace.define('ace/ext/beautify/php_rules', ['require', 'exports', 'module' , 'ace/token_iterator'], function(require, exports, module) {
var TokenIterator = require("ace/token_iterator").TokenIterator;
exports.newLines = [{
type: 'support.php_tag',
value: '<?php'
}, {
type: 'support.php_tag',
value: '<?'
}, {
type: 'support.php_tag',
value: '?>'
}, {
type: 'paren.lparen',
value: '{',
indent: true
}, {
type: 'paren.rparen',
breakBefore: true,
value: '}',
indent: false
}, {
type: 'paren.rparen',
breakBefore: true,
value: '})',
indent: false,
dontBreak: true
}, {
type: 'comment'
}, {
type: 'text',
value: ';'
}, {
type: 'text',
value: ':',
context: 'php'
}, {
type: 'keyword',
value: 'case',
indent: true,
dontBreak: true
}, {
type: 'keyword',
value: 'default',
indent: true,
dontBreak: true
}, {
type: 'keyword',
value: 'break',
indent: false,
dontBreak: true
}, {
type: 'punctuation.doctype.end',
value: '>'
}, {
type: 'meta.tag.punctuation.end',
value: '>'
}, {
type: 'meta.tag.punctuation.begin',
value: '<',
blockTag: true,
indent: true,
dontBreak: true
}, {
type: 'meta.tag.punctuation.begin',
value: '</',
indent: false,
breakBefore: true,
dontBreak: true
}, {
type: 'punctuation.operator',
value: ';'
}];
exports.spaces = [{
type: 'xml-pe',
prepend: true
},{
type: 'entity.other.attribute-name',
prepend: true
}, {
type: 'storage.type',
value: 'var',
append: true
}, {
type: 'storage.type',
value: 'function',
append: true
}, {
type: 'keyword.operator',
value: '='
}, {
type: 'keyword',
value: 'as',
prepend: true,
append: true
}, {
type: 'keyword',
value: 'function',
append: true
}, {
type: 'support.function',
next: /[^\(]/,
append: true
}, {
type: 'keyword',
value: 'or',
append: true,
prepend: true
}, {
type: 'keyword',
value: 'and',
append: true,
prepend: true
}, {
type: 'keyword',
value: 'case',
append: true
}, {
type: 'keyword.operator',
value: '||',
append: true,
prepend: true
}, {
type: 'keyword.operator',
value: '&&',
append: true,
prepend: true
}];
exports.singleTags = ['!doctype','area','base','br','hr','input','img','link','meta'];
exports.transform = function(iterator, maxPos, context) {
var token = iterator.getCurrentToken();
var newLines = exports.newLines;
var spaces = exports.spaces;
var singleTags = exports.singleTags;
var code = '';
var indentation = 0;
var dontBreak = false;
var tag;
var lastTag;
var lastToken = {};
var nextTag;
var nextToken = {};
var breakAdded = false;
var value = '';
while (token!==null) {
console.log(token);
if( !token ){
token = iterator.stepForward();
continue;
}
if( token.type == 'support.php_tag' && token.value != '?>' ){
context = 'php';
}
else if( token.type == 'support.php_tag' && token.value == '?>' ){
context = 'html';
}
else if( token.type == 'meta.tag.name.style' && context != 'css' ){
context = 'css';
}
else if( token.type == 'meta.tag.name.style' && context == 'css' ){
context = 'html';
}
else if( token.type == 'meta.tag.name.script' && context != 'js' ){
context = 'js';
}
else if( token.type == 'meta.tag.name.script' && context == 'js' ){
context = 'html';
}
nextToken = iterator.stepForward();
if (nextToken && nextToken.type.indexOf('meta.tag.name') == 0) {
nextTag = nextToken.value;
}
if ( lastToken.type == 'support.php_tag' && lastToken.value == '<?=') {
dontBreak = true;
}
if (token.type == 'meta.tag.name') {
token.value = token.value.toLowerCase();
}
if (token.type == 'text') {
token.value = token.value.trim();
}
if (!token.value) {
token = nextToken;
continue;
}
value = token.value;
for (var i in spaces) {
if (
token.type == spaces[i].type &&
(!spaces[i].value || token.value == spaces[i].value) &&
(
nextToken &&
(!spaces[i].next || spaces[i].next.test(nextToken.value))
)
) {
if (spaces[i].prepend) {
value = ' ' + token.value;
}
if (spaces[i].append) {
value += ' ';
}
}
}
if (token.type.indexOf('meta.tag.name') == 0) {
tag = token.value;
}
breakAdded = false;
for (i in newLines) {
if (
token.type == newLines[i].type &&
(
!newLines[i].value ||
token.value == newLines[i].value
) &&
(
!newLines[i].blockTag ||
singleTags.indexOf(nextTag) === -1
) &&
(
!newLines[i].context ||
newLines[i].context === context
)
) {
if (newLines[i].indent === false) {
indentation--;
}
if (
newLines[i].breakBefore &&
( !newLines[i].prev || newLines[i].prev.test(lastToken.value) )
) {
code += "\n";
breakAdded = true;
for (i = 0; i < indentation; i++) {
code += "\t";
}
}
break;
}
}
if (dontBreak===false) {
for (i in newLines) {
if (
lastToken.type == newLines[i].type &&
(
!newLines[i].value || lastToken.value == newLines[i].value
) &&
(
!newLines[i].blockTag ||
singleTags.indexOf(tag) === -1
) &&
(
!newLines[i].context ||
newLines[i].context === context
)
) {
if (newLines[i].indent === true) {
indentation++;
}
if (!newLines[i].dontBreak && !breakAdded) {
code += "\n";
for (i = 0; i < indentation; i++) {
code += "\t";
}
}
break;
}
}
}
code += value;
if ( lastToken.type == 'support.php_tag' && lastToken.value == '?>' ) {
dontBreak = false;
}
lastTag = tag;
lastToken = token;
token = nextToken;
if (token===null) {
break;
}
}
return code;
};
});

View File

@@ -1,537 +0,0 @@
ace.define('ace/ext/chromevox', ['require', 'exports', 'module' , 'ace/editor', 'ace/config'], function(require, exports, module) {
var cvoxAce = {};
cvoxAce.SpeechProperty;
cvoxAce.Cursor;
cvoxAce.Token;
cvoxAce.Annotation;
var CONSTANT_PROP = {
'rate': 0.8,
'pitch': 0.4,
'volume': 0.9
};
var DEFAULT_PROP = {
'rate': 1,
'pitch': 0.5,
'volume': 0.9
};
var ENTITY_PROP = {
'rate': 0.8,
'pitch': 0.8,
'volume': 0.9
};
var KEYWORD_PROP = {
'rate': 0.8,
'pitch': 0.3,
'volume': 0.9
};
var STORAGE_PROP = {
'rate': 0.8,
'pitch': 0.7,
'volume': 0.9
};
var VARIABLE_PROP = {
'rate': 0.8,
'pitch': 0.8,
'volume': 0.9
};
var DELETED_PROP = {
'punctuationEcho': 'none',
'relativePitch': -0.6
};
var ERROR_EARCON = 'ALERT_NONMODAL';
var MODE_SWITCH_EARCON = 'ALERT_MODAL';
var NO_MATCH_EARCON = 'INVALID_KEYPRESS';
var INSERT_MODE_STATE = 'insertMode';
var COMMAND_MODE_STATE = 'start';
var REPLACE_LIST = [
{
substr: ';',
newSubstr: ' semicolon '
},
{
substr: ':',
newSubstr: ' colon '
}
];
var Command = {
SPEAK_ANNOT: 'annots',
SPEAK_ALL_ANNOTS: 'all_annots',
TOGGLE_LOCATION: 'toggle_location',
SPEAK_MODE: 'mode',
SPEAK_ROW_COL: 'row_col',
TOGGLE_DISPLACEMENT: 'toggle_displacement',
FOCUS_TEXT: 'focus_text'
};
var KEY_PREFIX = 'CONTROL + SHIFT ';
cvoxAce.editor = null;
var lastCursor = null;
var annotTable = {};
var shouldSpeakRowLocation = false;
var shouldSpeakDisplacement = false;
var changed = false;
var vimState = null;
var keyCodeToShortcutMap = {};
var cmdToShortcutMap = {};
var getKeyShortcutString = function(keyCode) {
return KEY_PREFIX + String.fromCharCode(keyCode);
};
var isVimMode = function() {
var keyboardHandler = cvoxAce.editor.keyBinding.getKeyboardHandler();
return keyboardHandler.$id === 'ace/keyboard/vim';
};
var getCurrentToken = function(cursor) {
return cvoxAce.editor.getSession().getTokenAt(cursor.row, cursor.column + 1);
};
var getCurrentLine = function(cursor) {
return cvoxAce.editor.getSession().getLine(cursor.row);
};
var onRowChange = function(currCursor) {
if (annotTable[currCursor.row]) {
cvox.Api.playEarcon(ERROR_EARCON);
}
if (shouldSpeakRowLocation) {
cvox.Api.stop();
speakChar(currCursor);
speakTokenQueue(getCurrentToken(currCursor));
speakLine(currCursor.row, 1);
} else {
speakLine(currCursor.row, 0);
}
};
var isWord = function(cursor) {
var line = getCurrentLine(cursor);
var lineSuffix = line.substr(cursor.column - 1);
if (cursor.column === 0) {
lineSuffix = ' ' + line;
}
var firstWordRegExp = /^\W(\w+)/;
var words = firstWordRegExp.exec(lineSuffix);
return words !== null;
};
var rules = {
'constant': {
prop: CONSTANT_PROP
},
'entity': {
prop: ENTITY_PROP
},
'keyword': {
prop: KEYWORD_PROP
},
'storage': {
prop: STORAGE_PROP
},
'variable': {
prop: VARIABLE_PROP
},
'meta': {
prop: DEFAULT_PROP,
replace: [
{
substr: '</',
newSubstr: ' closing tag '
},
{
substr: '/>',
newSubstr: ' close tag '
},
{
substr: '<',
newSubstr: ' tag start '
},
{
substr: '>',
newSubstr: ' tag end '
}
]
}
};
var DEFAULT_RULE = {
prop: DEFAULT_RULE
};
var expand = function(value, replaceRules) {
var newValue = value;
for (var i = 0; i < replaceRules.length; i++) {
var replaceRule = replaceRules[i];
var regexp = new RegExp(replaceRule.substr, 'g');
newValue = newValue.replace(regexp, replaceRule.newSubstr);
}
return newValue;
};
var mergeTokens = function(tokens, start, end) {
var newToken = {};
newToken.value = '';
newToken.type = tokens[start].type;
for (var j = start; j < end; j++) {
newToken.value += tokens[j].value;
}
return newToken;
};
var mergeLikeTokens = function(tokens) {
if (tokens.length <= 1) {
return tokens;
}
var newTokens = [];
var lastLikeIndex = 0;
for (var i = 1; i < tokens.length; i++) {
var lastLikeToken = tokens[lastLikeIndex];
var currToken = tokens[i];
if (getTokenRule(lastLikeToken) !== getTokenRule(currToken)) {
newTokens.push(mergeTokens(tokens, lastLikeIndex, i));
lastLikeIndex = i;
}
}
newTokens.push(mergeTokens(tokens, lastLikeIndex, tokens.length));
return newTokens;
};
var isRowWhiteSpace = function(row) {
var line = cvoxAce.editor.getSession().getLine(row);
var whiteSpaceRegexp = /^\s*$/;
return whiteSpaceRegexp.exec(line) !== null;
};
var speakLine = function(row, queue) {
var tokens = cvoxAce.editor.getSession().getTokens(row);
if (tokens.length === 0 || isRowWhiteSpace(row)) {
cvox.Api.playEarcon('EDITABLE_TEXT');
return;
}
tokens = mergeLikeTokens(tokens);
var firstToken = tokens[0];
tokens = tokens.filter(function(token) {
return token !== firstToken;
});
speakToken_(firstToken, queue);
tokens.forEach(speakTokenQueue);
};
var speakTokenFlush = function(token) {
speakToken_(token, 0);
};
var speakTokenQueue = function(token) {
speakToken_(token, 1);
};
var getTokenRule = function(token) {
if (!token || !token.type) {
return;
}
var split = token.type.split('.');
if (split.length === 0) {
return;
}
var type = split[0];
var rule = rules[type];
if (!rule) {
return DEFAULT_RULE;
}
return rule;
};
var speakToken_ = function(token, queue) {
var rule = getTokenRule(token);
var value = expand(token.value, REPLACE_LIST);
if (rule.replace) {
value = expand(value, rule.replace);
}
cvox.Api.speak(value, queue, rule.prop);
};
var speakChar = function(cursor) {
var line = getCurrentLine(cursor);
cvox.Api.speak(line[cursor.column], 1);
};
var speakDisplacement = function(lastCursor, currCursor) {
var line = getCurrentLine(currCursor);
var displace = line.substring(lastCursor.column, currCursor.column);
displace = displace.replace(/ /g, ' space ');
cvox.Api.speak(displace);
};
var speakCharOrWordOrLine = function(lastCursor, currCursor) {
if (Math.abs(lastCursor.column - currCursor.column) !== 1) {
var currLineLength = getCurrentLine(currCursor).length;
if (currCursor.column === 0 || currCursor.column === currLineLength) {
speakLine(currCursor.row, 0);
return;
}
if (isWord(currCursor)) {
cvox.Api.stop();
speakTokenQueue(getCurrentToken(currCursor));
return;
}
}
speakChar(currCursor);
};
var onColumnChange = function(lastCursor, currCursor) {
if (!cvoxAce.editor.selection.isEmpty()) {
speakDisplacement(lastCursor, currCursor);
cvox.Api.speak('selected', 1);
}
else if (shouldSpeakDisplacement) {
speakDisplacement(lastCursor, currCursor);
} else {
speakCharOrWordOrLine(lastCursor, currCursor);
}
};
var onCursorChange = function(evt) {
if (changed) {
changed = false;
return;
}
var currCursor = cvoxAce.editor.selection.getCursor();
if (currCursor.row !== lastCursor.row) {
onRowChange(currCursor);
} else {
onColumnChange(lastCursor, currCursor);
}
lastCursor = currCursor;
};
var onSelectionChange = function(evt) {
if (cvoxAce.editor.selection.isEmpty()) {
cvox.Api.speak('unselected');
}
};
var onChange = function(evt) {
var data = evt.data;
switch (data.action) {
case 'removeText':
cvox.Api.speak(data.text, 0, DELETED_PROP);
changed = true;
break;
case 'insertText':
cvox.Api.speak(data.text, 0);
changed = true;
break;
}
};
var isNewAnnotation = function(annot) {
var row = annot.row;
var col = annot.column;
return !annotTable[row] || !annotTable[row][col];
};
var populateAnnotations = function(annotations) {
annotTable = {};
for (var i = 0; i < annotations.length; i++) {
var annotation = annotations[i];
var row = annotation.row;
var col = annotation.column;
if (!annotTable[row]) {
annotTable[row] = {};
}
annotTable[row][col] = annotation;
}
};
var onAnnotationChange = function(evt) {
var annotations = cvoxAce.editor.getSession().getAnnotations();
var newAnnotations = annotations.filter(isNewAnnotation);
if (newAnnotations.length > 0) {
cvox.Api.playEarcon(ERROR_EARCON);
}
populateAnnotations(annotations);
};
var speakAnnot = function(annot) {
var annotText = annot.type + ' ' + annot.text + ' on ' +
rowColToString(annot.row, annot.column);
annotText = annotText.replace(';', 'semicolon');
cvox.Api.speak(annotText, 1);
};
var speakAnnotsByRow = function(row) {
var annots = annotTable[row];
for (var col in annots) {
speakAnnot(annots[col]);
}
};
var rowColToString = function(row, col) {
return 'row ' + (row + 1) + ' column ' + (col + 1);
};
var speakCurrRowAndCol = function() {
cvox.Api.speak(rowColToString(lastCursor.row, lastCursor.column));
};
var speakAllAnnots = function() {
for (var row in annotTable) {
speakAnnotsByRow(row);
}
};
var speakMode = function() {
if (!isVimMode()) {
return;
}
switch (cvoxAce.editor.keyBinding.$data.state) {
case INSERT_MODE_STATE:
cvox.Api.speak('Insert mode');
break;
case COMMAND_MODE_STATE:
cvox.Api.speak('Command mode');
break;
}
};
var toggleSpeakRowLocation = function() {
shouldSpeakRowLocation = !shouldSpeakRowLocation;
if (shouldSpeakRowLocation) {
cvox.Api.speak('Speak location on row change enabled.');
} else {
cvox.Api.speak('Speak location on row change disabled.');
}
};
var toggleSpeakDisplacement = function() {
shouldSpeakDisplacement = !shouldSpeakDisplacement;
if (shouldSpeakDisplacement) {
cvox.Api.speak('Speak displacement on column changes.');
} else {
cvox.Api.speak('Speak current character or word on column changes.');
}
};
var onKeyDown = function(evt) {
if (evt.ctrlKey && evt.shiftKey) {
var shortcut = keyCodeToShortcutMap[evt.keyCode];
if (shortcut) {
shortcut.func();
}
}
};
var onChangeStatus = function(evt, editor) {
if (!isVimMode()) {
return;
}
var state = editor.keyBinding.$data.state;
if (state === vimState) {
return;
}
switch (state) {
case INSERT_MODE_STATE:
cvox.Api.playEarcon(MODE_SWITCH_EARCON);
cvox.Api.setKeyEcho(true);
break;
case COMMAND_MODE_STATE:
cvox.Api.playEarcon(MODE_SWITCH_EARCON);
cvox.Api.setKeyEcho(false);
break;
}
vimState = state;
};
var contextMenuHandler = function(evt) {
var cmd = evt.detail['customCommand'];
var shortcut = cmdToShortcutMap[cmd];
if (shortcut) {
shortcut.func();
cvoxAce.editor.focus();
}
};
var initContextMenu = function() {
var ACTIONS = SHORTCUTS.map(function(shortcut) {
return {
desc: shortcut.desc + getKeyShortcutString(shortcut.keyCode),
cmd: shortcut.cmd
};
});
var body = document.querySelector('body');
body.setAttribute('contextMenuActions', JSON.stringify(ACTIONS));
body.addEventListener('ATCustomEvent', contextMenuHandler, true);
};
var onFindSearchbox = function(evt) {
if (evt.match) {
speakLine(lastCursor.row, 0);
} else {
cvox.Api.playEarcon(NO_MATCH_EARCON);
}
};
var focus = function() {
cvoxAce.editor.focus();
};
var SHORTCUTS = [
{
keyCode: 49,
func: function() {
speakAnnotsByRow(lastCursor.row);
},
cmd: Command.SPEAK_ANNOT,
desc: 'Speak annotations on line'
},
{
keyCode: 50,
func: speakAllAnnots,
cmd: Command.SPEAK_ALL_ANNOTS,
desc: 'Speak all annotations'
},
{
keyCode: 51,
func: speakMode,
cmd: Command.SPEAK_MODE,
desc: 'Speak Vim mode'
},
{
keyCode: 52,
func: toggleSpeakRowLocation,
cmd: Command.TOGGLE_LOCATION,
desc: 'Toggle speak row location'
},
{
keyCode: 53,
func: speakCurrRowAndCol,
cmd: Command.SPEAK_ROW_COL,
desc: 'Speak row and column'
},
{
keyCode: 54,
func: toggleSpeakDisplacement,
cmd: Command.TOGGLE_DISPLACEMENT,
desc: 'Toggle speak displacement'
},
{
keyCode: 55,
func: focus,
cmd: Command.FOCUS_TEXT,
desc: 'Focus text'
}
];
var onFocus = function() {
cvoxAce.editor = editor;
editor.getSession().selection.on('changeCursor', onCursorChange);
editor.getSession().selection.on('changeSelection', onSelectionChange);
editor.getSession().on('change', onChange);
editor.getSession().on('changeAnnotation', onAnnotationChange);
editor.on('changeStatus', onChangeStatus);
editor.on('findSearchBox', onFindSearchbox);
editor.container.addEventListener('keydown', onKeyDown);
lastCursor = editor.selection.getCursor();
};
var init = function(editor) {
onFocus();
SHORTCUTS.forEach(function(shortcut) {
keyCodeToShortcutMap[shortcut.keyCode] = shortcut;
cmdToShortcutMap[shortcut.cmd] = shortcut;
});
editor.on('focus', onFocus);
if (isVimMode()) {
cvox.Api.setKeyEcho(false);
}
initContextMenu();
};
function cvoxApiExists() {
return (typeof(cvox) !== 'undefined') && cvox && cvox.Api;
}
var tries = 0;
var MAX_TRIES = 15;
function watchForCvoxLoad(editor) {
if (cvoxApiExists()) {
init(editor);
} else {
tries++;
if (tries >= MAX_TRIES) {
return;
}
window.setTimeout(watchForCvoxLoad, 500, editor);
}
}
var Editor = require('../editor').Editor;
require('../config').defineOptions(Editor.prototype, 'editor', {
enableChromevoxEnhancements: {
set: function(val) {
if (val) {
watchForCvoxLoad(this);
}
},
value: true // turn it on by default or check for window.cvox
}
});
});

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