diff --git a/README.md b/README.md index 41c57d1..f47318c 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ - File editing in repository viewer - Comment for the changeset - Network graph -- Statics +- Statistics - Watch / Star If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki). @@ -35,6 +35,8 @@ 2. Deploy it to the Servlet 3.0 container such as Tomcat 7.x, Jetty 8.x, GlassFish 3.x or higher. 3. Access **http://[hostname]:[port]/gitbucket/** using your web browser. +If you are using Gitbucket behind a webserver please make sure you have increased the **client_max_body_size** (on nignx) + The default administrator account is **root** and password is **root**. or you can start GitBucket by `java -jar gitbucket.war` without servlet container. In this case, GitBucket URL is **http://[hostname]:8080/**. You can specify following options. @@ -58,6 +60,19 @@ Release Notes -------- +### 1.12 - 29 Mar 2014 +- SSH repository access is available +- Allow users can create and management their groups +- Git submodule support +- Close issues via commit messages +- Show repository description below the name on repository page +- Fix presentation of the source viewer +- Upgrade to sbt 0.13 +- Fix some bugs + +### 1.11.1 - 06 Mar 2014 +- Bug fix + ### 1.11 - 01 Mar 2014 - Base URL for redirection, notification and repository URL box is configurable - Remove ```--https``` option because it's possible to substitute in the base url diff --git a/project/build.properties b/project/build.properties index db255c2..37b489c 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.12.3 \ No newline at end of file +sbt.version=0.13.1 diff --git a/project/build.scala b/project/build.scala index bcf125e..9d2e16d 100644 --- a/project/build.scala +++ b/project/build.scala @@ -31,12 +31,13 @@ "org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test", "org.scalatra" %% "scalatra-json" % ScalatraVersion, "org.json4s" %% "json4s-jackson" % "3.2.5", - "jp.sf.amateras" %% "scalatra-forms" % "0.0.11", + "jp.sf.amateras" %% "scalatra-forms" % "0.0.14", "commons-io" % "commons-io" % "2.4", "org.pegdown" % "pegdown" % "1.4.1", "org.apache.commons" % "commons-compress" % "1.5", "org.apache.commons" % "commons-email" % "1.3.1", "org.apache.httpcomponents" % "httpclient" % "4.3", + "org.apache.sshd" % "apache-sshd" % "0.10.0", "com.typesafe.slick" %% "slick" % "1.0.1", "com.novell.ldap" % "jldap" % "2009-10-07", "com.h2database" % "h2" % "1.3.173", diff --git a/project/plugins.sbt b/project/plugins.sbt index 15ac806..7d7ab3e 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,9 +1,11 @@ -addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.2.0") +addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.4.0") -addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.1") +addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0") -addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.0") +addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.5") -addSbtPlugin("io.spray" % "sbt-twirl" % "0.6.1") +resolvers += "spray repo" at "http://repo.spray.io" -addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.2") +addSbtPlugin("io.spray" % "sbt-twirl" % "0.7.0") + +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.4") diff --git a/sbt-launch-0.12.3.jar b/sbt-launch-0.12.3.jar deleted file mode 100644 index 672c26f..0000000 --- a/sbt-launch-0.12.3.jar +++ /dev/null Binary files differ diff --git a/sbt-launch-0.13.1.jar b/sbt-launch-0.13.1.jar new file mode 100644 index 0000000..5c7d052 --- /dev/null +++ b/sbt-launch-0.13.1.jar Binary files differ diff --git a/sbt.bat b/sbt.bat index d86d1e0..cd356dd 100644 --- a/sbt.bat +++ b/sbt.bat @@ -1,2 +1,2 @@ set SCRIPT_DIR=%~dp0 -java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar "%SCRIPT_DIR%\sbt-launch-0.12.3.jar" %* +java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.1.jar" %* diff --git a/sbt.sh b/sbt.sh index 23c721f..86cf93e 100755 --- a/sbt.sh +++ b/sbt.sh @@ -1 +1 @@ -java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar `dirname $0`/sbt-launch-0.12.3.jar "$@" +java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.1.jar "$@" diff --git a/src/main/resources/update/1_12.sql b/src/main/resources/update/1_12.sql new file mode 100644 index 0000000..f8658a2 --- /dev/null +++ b/src/main/resources/update/1_12.sql @@ -0,0 +1,11 @@ +ALTER TABLE GROUP_MEMBER ADD COLUMN MANAGER BOOLEAN DEFAULT FALSE; + +CREATE TABLE SSH_KEY ( + USER_NAME VARCHAR(100) NOT NULL, + SSH_KEY_ID INT AUTO_INCREMENT, + TITLE VARCHAR(100) NOT NULL, + PUBLIC_KEY TEXT NOT NULL +); + +ALTER TABLE SSH_KEY ADD CONSTRAINT IDX_SSH_KEY_PK PRIMARY KEY (USER_NAME, SSH_KEY_ID); +ALTER TABLE SSH_KEY ADD CONSTRAINT IDX_SSH_KEY_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME); diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala index de3c40d..21c213d 100644 --- a/src/main/scala/ScalatraBootstrap.scala +++ b/src/main/scala/ScalatraBootstrap.scala @@ -20,7 +20,6 @@ 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, "/*") diff --git a/src/main/scala/app/AccountController.scala b/src/main/scala/app/AccountController.scala index df24aad..d19af7e 100644 --- a/src/main/scala/app/AccountController.scala +++ b/src/main/scala/app/AccountController.scala @@ -1,17 +1,25 @@ package app import service._ -import util.{FileUtil, OneselfAuthenticator} +import util._ import util.StringUtil._ import util.Directory._ +import util.ControlUtil._ import jp.sf.amateras.scalatra.forms._ import org.apache.commons.io.FileUtils +import org.scalatra.i18n.Messages +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.{FileMode, Constants} +import org.eclipse.jgit.dircache.DirCache +import model.GroupMember class AccountController extends AccountControllerBase - with AccountService with RepositoryService with ActivityService with OneselfAuthenticator + with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService + with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator trait AccountControllerBase extends AccountManagementControllerBase { - self: AccountService with RepositoryService with ActivityService with OneselfAuthenticator => + self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService + with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator => case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String, url: Option[String], fileId: Option[String]) @@ -19,6 +27,8 @@ case class AccountEditForm(password: Option[String], fullName: String, mailAddress: String, url: Option[String], fileId: Option[String], clearImage: Boolean) + case class SshKeyForm(title: String, publicKey: String) + val newForm = mapping( "userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))), "password" -> trim(label("Password" , text(required, maxlength(20)))), @@ -37,6 +47,45 @@ "clearImage" -> trim(label("Clear image" , boolean())) )(AccountEditForm.apply) + val sshKeyForm = mapping( + "title" -> trim(label("Title", text(required, maxlength(100)))), + "publicKey" -> trim(label("Key" , text(required))) + )(SshKeyForm.apply) + + case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String) + case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String, clearImage: Boolean) + + val newGroupForm = mapping( + "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "members" -> trim(label("Members" ,text(required, members))) + )(NewGroupForm.apply) + + val editGroupForm = mapping( + "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "members" -> trim(label("Members" ,text(required, members))), + "clearImage" -> trim(label("Clear image" ,boolean())) + )(EditGroupForm.apply) + + case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) + case class ForkRepositoryForm(owner: String, name: String) + + val newRepositoryForm = mapping( + "owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))), + "name" -> trim(label("Repository name", text(required, maxlength(40), identifier, uniqueRepository))), + "description" -> trim(label("Description" , optional(text()))), + "isPrivate" -> trim(label("Repository Type", boolean())), + "createReadme" -> trim(label("Create README" , boolean())) + )(RepositoryCreationForm.apply) + + val forkRepositoryForm = mapping( + "owner" -> trim(label("Repository owner", text(required))), + "name" -> trim(label("Repository name", text(required))) + )(ForkRepositoryForm.apply) + /** * Displays user information. */ @@ -51,14 +100,20 @@ getActivitiesByUser(userName, true)) // Members - case "members" if(account.isGroupAccount) => - _root_.account.html.members(account, getGroupMembers(account.userName)) + case "members" if(account.isGroupAccount) => { + val members = getGroupMembers(account.userName) + _root_.account.html.members(account, members.map(_.userName), + context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })) + } // Repositories - case _ => + case _ => { + val members = getGroupMembers(account.userName) _root_.account.html.repositories(account, if(account.isGroupAccount) Nil else getGroupsByUserName(userName), - getVisibleRepositories(context.loginAccount, baseUrl, Some(userName))) + getVisibleRepositories(context.loginAccount, baseUrl, Some(userName)), + context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })) + } } } getOrElse NotFound } @@ -76,7 +131,9 @@ get("/:userName/_edit")(oneselfOnly { val userName = params("userName") - getAccountByUserName(userName).map(x => account.html.edit(Some(x), flash.get("info"))) getOrElse NotFound + getAccountByUserName(userName).map { x => + account.html.edit(x, flash.get("info")) + } getOrElse NotFound }) post("/:userName/_edit", editForm)(oneselfOnly { form => @@ -116,22 +173,266 @@ redirect("/") }) + get("/:userName/_ssh")(oneselfOnly { + val userName = params("userName") + getAccountByUserName(userName).map { x => + account.html.ssh(x, getPublicKeys(x.userName)) + } getOrElse NotFound + }) + + post("/:userName/_ssh", sshKeyForm)(oneselfOnly { form => + val userName = params("userName") + addPublicKey(userName, form.title, form.publicKey) + redirect(s"/${userName}/_ssh") + }) + + get("/:userName/_ssh/delete/:id")(oneselfOnly { + val userName = params("userName") + val sshKeyId = params("id").toInt + deletePublicKey(userName, sshKeyId) + redirect(s"/${userName}/_ssh") + }) + get("/register"){ - if(loadSystemSettings().allowAccountRegistration){ + if(context.settings.allowAccountRegistration){ if(context.loginAccount.isDefined){ redirect("/") } else { - account.html.edit(None, None) + account.html.register() } } else NotFound } post("/register", newForm){ form => - if(loadSystemSettings().allowAccountRegistration){ + if(context.settings.allowAccountRegistration){ createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url) updateImage(form.userName, form.fileId, false) redirect("/signin") } else NotFound } + get("/groups/new")(usersOnly { + account.html.group(None, List(GroupMember("", context.loginAccount.get.userName, true))) + }) + + post("/groups/new", newGroupForm)(usersOnly { form => + createGroup(form.groupName, form.url) + updateGroupMembers(form.groupName, form.members.split(",").map { + _.split(":") match { + case Array(userName, isManager) => (userName, isManager.toBoolean) + } + }.toList) + updateImage(form.groupName, form.fileId, false) + redirect(s"/${form.groupName}") + }) + + get("/:groupName/_editgroup")(managersOnly { + defining(params("groupName")){ groupName => + account.html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName)) + } + }) + + get("/:groupName/_deletegroup")(managersOnly { + defining(params("groupName")){ groupName => + // Remove from GROUP_MEMBER + updateGroupMembers(groupName, Nil) + // Remove repositories + getRepositoryNamesOfUser(groupName).foreach { repositoryName => + deleteRepository(groupName, repositoryName) + FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName)) + FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName)) + FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName)) + } + } + redirect("/") + }) + + post("/:groupName/_editgroup", editGroupForm)(managersOnly { form => + defining(params("groupName"), form.members.split(",").map { + _.split(":") match { + case Array(userName, isManager) => (userName, isManager.toBoolean) + } + }.toList){ case (groupName, members) => + getAccountByUserName(groupName, true).map { account => + updateGroup(groupName, form.url, false) + + // Update GROUP_MEMBER + updateGroupMembers(form.groupName, members) + // Update COLLABORATOR for group repositories + getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => + removeCollaborators(form.groupName, repositoryName) + members.foreach { case (userName, isManager) => + addCollaborator(form.groupName, repositoryName, userName) + } + } + + updateImage(form.groupName, form.fileId, form.clearImage) + redirect(s"/${form.groupName}") + + } getOrElse NotFound + } + }) + + /** + * Show the new repository form. + */ + get("/new")(usersOnly { + account.html.newrepo(getGroupsByUserName(context.loginAccount.get.userName)) + }) + + /** + * Create new repository. + */ + post("/new", newRepositoryForm)(usersOnly { form => + LockUtil.lock(s"${form.owner}/${form.name}/create"){ + if(getRepository(form.owner, form.name, baseUrl).isEmpty){ + val ownerAccount = getAccountByUserName(form.owner).get + val loginAccount = context.loginAccount.get + val loginUserName = loginAccount.userName + + // Insert to the database at first + createRepository(form.name, form.owner, form.description, form.isPrivate) + + // Add collaborators for group repository + if(ownerAccount.isGroupAccount){ + getGroupMembers(form.owner).foreach { member => + addCollaborator(form.owner, form.name, member.userName) + } + } + + // Insert default labels + insertDefaultLabels(form.owner, form.name) + + // Create the actual repository + val gitdir = getRepositoryDir(form.owner, form.name) + JGitUtil.initRepository(gitdir) + + if(form.createReadme){ + using(Git.open(gitdir)){ git => + val builder = DirCache.newInCore.builder() + val inserter = git.getRepository.newObjectInserter() + val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") + val content = if(form.description.nonEmpty){ + form.name + "\n" + + "===============\n" + + "\n" + + form.description.get + } else { + form.name + "\n" + + "===============\n" + } + + builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE, + inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")))) + builder.finish() + + JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), + loginAccount.fullName, loginAccount.mailAddress, "Initial commit") + } + } + + // Create Wiki repository + createWikiRepository(loginAccount, form.owner, form.name) + + // Record activity + recordCreateRepositoryActivity(form.owner, form.name, loginUserName) + } + + // redirect to the repository + redirect(s"/${form.owner}/${form.name}") + } + }) + + get("/:owner/:repository/fork")(readableUsersOnly { repository => + val loginAccount = context.loginAccount.get + val loginUserName = loginAccount.userName + + LockUtil.lock(s"${loginUserName}/${repository.name}/create"){ + if(repository.owner == loginUserName){ + // redirect to the repository + redirect(s"/${repository.owner}/${repository.name}") + } else { + getForkedRepositories(repository.owner, repository.name).find(_._1 == loginUserName).map { case (owner, name) => + // redirect to the repository + redirect(s"/${owner}/${name}") + } getOrElse { + // Insert to the database at first + val originUserName = repository.repository.originUserName.getOrElse(repository.owner) + val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name) + + createRepository( + repositoryName = repository.name, + userName = loginUserName, + description = repository.repository.description, + isPrivate = repository.repository.isPrivate, + originRepositoryName = Some(originRepositoryName), + originUserName = Some(originUserName), + parentRepositoryName = Some(repository.name), + parentUserName = Some(repository.owner) + ) + + // Insert default labels + insertDefaultLabels(loginUserName, repository.name) + + // clone repository actually + JGitUtil.cloneRepository( + getRepositoryDir(repository.owner, repository.name), + getRepositoryDir(loginUserName, repository.name)) + + // Create Wiki repository + JGitUtil.cloneRepository( + getWikiRepositoryDir(repository.owner, repository.name), + getWikiRepositoryDir(loginUserName, repository.name)) + + // insert commit id + using(Git.open(getRepositoryDir(loginUserName, repository.name))){ git => + JGitUtil.getRepositoryInfo(loginUserName, repository.name, baseUrl).branchList.foreach { branch => + JGitUtil.getCommitLog(git, branch) match { + case Right((commits, _)) => commits.foreach { commit => + if(!existsCommitId(loginUserName, repository.name, commit.id)){ + insertCommitId(loginUserName, repository.name, commit.id) + } + } + case Left(_) => ??? + } + } + } + + // Record activity + recordForkActivity(repository.owner, repository.name, loginUserName) + // redirect to the repository + redirect(s"/${loginUserName}/${repository.name}") + } + } + } + }) + + private def insertDefaultLabels(userName: String, repositoryName: String): Unit = { + createLabel(userName, repositoryName, "bug", "fc2929") + createLabel(userName, repositoryName, "duplicate", "cccccc") + createLabel(userName, repositoryName, "enhancement", "84b6eb") + createLabel(userName, repositoryName, "invalid", "e6e6e6") + createLabel(userName, repositoryName, "question", "cc317c") + createLabel(userName, repositoryName, "wontfix", "ffffff") + } + + private def existsAccount: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = + if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None + } + + private def uniqueRepository: Constraint = new Constraint(){ + override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = + params.get("owner").flatMap { userName => + getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.") + } + } + + private def members: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = { + if(value.split(",").exists { + _.split(":") match { case Array(userName, isManager) => isManager.toBoolean } + }) None else Some("Must select one manager at least.") + } + } } diff --git a/src/main/scala/app/ControllerBase.scala b/src/main/scala/app/ControllerBase.scala index cf91fea..d7d7171 100644 --- a/src/main/scala/app/ControllerBase.scala +++ b/src/main/scala/app/ControllerBase.scala @@ -28,7 +28,7 @@ // Don't set content type via Accept header. override def format(implicit request: HttpServletRequest) = "" - override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { + override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = try { val httpRequest = request.asInstanceOf[HttpServletRequest] val httpResponse = response.asInstanceOf[HttpServletResponse] val context = request.getServletContext.getContextPath @@ -36,15 +36,16 @@ if(path.startsWith("/console/")){ val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account] + val baseUrl = this.baseUrl(httpRequest) if(account == null){ // Redirect to login form - httpResponse.sendRedirect(context + "/signin?" + StringUtil.urlEncode(path)) + httpResponse.sendRedirect(baseUrl + "/signin?redirect=" + StringUtil.urlEncode(path)) } else if(account.isAdmin){ // H2 Console (administrators only) chain.doFilter(request, response) } else { // Redirect to dashboard - httpResponse.sendRedirect(context + "/") + httpResponse.sendRedirect(baseUrl + "/") } } else if(path.startsWith("/git/")){ // Git repository @@ -53,12 +54,25 @@ // Scalatra actions super.doFilter(request, response, chain) } + } finally { + contextCache.remove(); } + private val contextCache = new java.lang.ThreadLocal[Context]() + /** * Returns the context object for the request. */ - implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, request) + implicit def context: Context = { + contextCache.get match { + case null => { + val context = Context(loadSystemSettings(), LoginAccount, request) + contextCache.set(context) + context + } + case context => context + } + } private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount) @@ -116,14 +130,18 @@ includeContextPath: Boolean = true, includeServletPath: Boolean = true) (implicit request: HttpServletRequest, response: HttpServletResponse) = if (path.startsWith("http")) path - else baseUrl + url(path, params, includeContextPath, includeServletPath) + else baseUrl + url(path, params, false, false, false) } /** * Context object for the current request. */ -case class Context(path: String, loginAccount: Option[Account], request: HttpServletRequest){ +case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){ + + lazy val path = settings.baseUrl.getOrElse(request.getServletContext.getContextPath) + + lazy val currentPath = request.getRequestURI.substring(request.getContextPath.length) /** * Get object from cache. diff --git a/src/main/scala/app/CreateRepositoryController.scala b/src/main/scala/app/CreateRepositoryController.scala deleted file mode 100644 index 16cd05c..0000000 --- a/src/main/scala/app/CreateRepositoryController.scala +++ /dev/null @@ -1,199 +0,0 @@ -package app - -import util.Directory._ -import util.ControlUtil._ -import util._ -import service._ -import org.eclipse.jgit.api.Git -import jp.sf.amateras.scalatra.forms._ -import org.eclipse.jgit.lib.{FileMode, Constants} -import org.eclipse.jgit.dircache.DirCache -import org.scalatra.i18n.Messages - -class CreateRepositoryController extends CreateRepositoryControllerBase - with RepositoryService with AccountService with WikiService with LabelsService with ActivityService - with UsersAuthenticator with ReadableUsersAuthenticator - -/** - * Creates new repository. - */ -trait CreateRepositoryControllerBase extends ControllerBase { - self: RepositoryService with AccountService with WikiService with LabelsService with ActivityService - with UsersAuthenticator with ReadableUsersAuthenticator => - - case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) - - case class ForkRepositoryForm(owner: String, name: String) - - val newForm = mapping( - "owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))), - "name" -> trim(label("Repository name", text(required, maxlength(40), identifier, unique))), - "description" -> trim(label("Description" , optional(text()))), - "isPrivate" -> trim(label("Repository Type", boolean())), - "createReadme" -> trim(label("Create README" , boolean())) - )(RepositoryCreationForm.apply) - - val forkForm = mapping( - "owner" -> trim(label("Repository owner", text(required))), - "name" -> trim(label("Repository name", text(required))) - )(ForkRepositoryForm.apply) - - /** - * Show the new repository form. - */ - get("/new")(usersOnly { - html.newrepo(getGroupsByUserName(context.loginAccount.get.userName)) - }) - - /** - * Create new repository. - */ - post("/new", newForm)(usersOnly { form => - LockUtil.lock(s"${form.owner}/${form.name}/create"){ - if(getRepository(form.owner, form.name, baseUrl).isEmpty){ - val ownerAccount = getAccountByUserName(form.owner).get - val loginAccount = context.loginAccount.get - val loginUserName = loginAccount.userName - - // Insert to the database at first - createRepository(form.name, form.owner, form.description, form.isPrivate) - - // Add collaborators for group repository - if(ownerAccount.isGroupAccount){ - getGroupMembers(form.owner).foreach { userName => - addCollaborator(form.owner, form.name, userName) - } - } - - // Insert default labels - insertDefaultLabels(form.owner, form.name) - - // Create the actual repository - val gitdir = getRepositoryDir(form.owner, form.name) - JGitUtil.initRepository(gitdir) - - if(form.createReadme){ - using(Git.open(gitdir)){ git => - val builder = DirCache.newInCore.builder() - val inserter = git.getRepository.newObjectInserter() - val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") - val content = if(form.description.nonEmpty){ - form.name + "\n" + - "===============\n" + - "\n" + - form.description.get - } else { - form.name + "\n" + - "===============\n" - } - - builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE, - inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")))) - builder.finish() - - JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), - loginAccount.fullName, loginAccount.mailAddress, "Initial commit") - } - } - - // Create Wiki repository - createWikiRepository(loginAccount, form.owner, form.name) - - // Record activity - recordCreateRepositoryActivity(form.owner, form.name, loginUserName) - } - - // redirect to the repository - redirect(s"/${form.owner}/${form.name}") - } - }) - - get("/:owner/:repository/fork")(readableUsersOnly { repository => - val loginAccount = context.loginAccount.get - val loginUserName = loginAccount.userName - - LockUtil.lock(s"${loginUserName}/${repository.name}/create"){ - if(repository.owner == loginUserName){ - // redirect to the repository - redirect(s"/${repository.owner}/${repository.name}") - } else { - getForkedRepositories(repository.owner, repository.name).find(_._1 == loginUserName).map { case (owner, name) => - // redirect to the repository - redirect(s"/${owner}/${name}") - } getOrElse { - // Insert to the database at first - val originUserName = repository.repository.originUserName.getOrElse(repository.owner) - val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name) - - createRepository( - repositoryName = repository.name, - userName = loginUserName, - description = repository.repository.description, - isPrivate = repository.repository.isPrivate, - originRepositoryName = Some(originRepositoryName), - originUserName = Some(originUserName), - parentRepositoryName = Some(repository.name), - parentUserName = Some(repository.owner) - ) - - // Insert default labels - insertDefaultLabels(loginUserName, repository.name) - - // clone repository actually - JGitUtil.cloneRepository( - getRepositoryDir(repository.owner, repository.name), - getRepositoryDir(loginUserName, repository.name)) - - // Create Wiki repository - JGitUtil.cloneRepository( - getWikiRepositoryDir(repository.owner, repository.name), - getWikiRepositoryDir(loginUserName, repository.name)) - - // insert commit id - using(Git.open(getRepositoryDir(loginUserName, repository.name))){ git => - JGitUtil.getRepositoryInfo(loginUserName, repository.name, baseUrl).branchList.foreach { branch => - JGitUtil.getCommitLog(git, branch) match { - case Right((commits, _)) => commits.foreach { commit => - if(!existsCommitId(loginUserName, repository.name, commit.id)){ - insertCommitId(loginUserName, repository.name, commit.id) - } - } - case Left(_) => ??? - } - } - } - - // Record activity - recordForkActivity(repository.owner, repository.name, loginUserName) - // redirect to the repository - redirect(s"/${loginUserName}/${repository.name}") - } - } - } - }) - - private def insertDefaultLabels(userName: String, repositoryName: String): Unit = { - createLabel(userName, repositoryName, "bug", "fc2929") - createLabel(userName, repositoryName, "duplicate", "cccccc") - createLabel(userName, repositoryName, "enhancement", "84b6eb") - createLabel(userName, repositoryName, "invalid", "e6e6e6") - createLabel(userName, repositoryName, "question", "cc317c") - createLabel(userName, repositoryName, "wontfix", "ffffff") - } - - private def existsAccount: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = - if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None - } - - /** - * Duplicate check for the repository name. - */ - private def unique: Constraint = new Constraint(){ - override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = - params.get("owner").flatMap { userName => - getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.") - } - } - -} diff --git a/src/main/scala/app/IndexController.scala b/src/main/scala/app/IndexController.scala index 0e46b3c..426ec63 100644 --- a/src/main/scala/app/IndexController.scala +++ b/src/main/scala/app/IndexController.scala @@ -22,7 +22,6 @@ html.index(getRecentActivities(), getVisibleRepositories(loginAccount, baseUrl), - loadSystemSettings(), loginAccount.map{ account => getUserRepositories(account.userName, baseUrl) }.getOrElse(Nil) ) } @@ -32,11 +31,11 @@ if(redirect.isDefined && redirect.get.startsWith("/")){ flash += Keys.Flash.Redirect -> redirect.get } - html.signin(loadSystemSettings()) + html.signin() } post("/signin", form){ form => - authenticate(loadSystemSettings(), form.userName, form.password) match { + authenticate(context.settings, form.userName, form.password) match { case Some(account) => signin(account) case None => redirect("/signin") } diff --git a/src/main/scala/app/PullRequestsController.scala b/src/main/scala/app/PullRequestsController.scala index bf3bea2..47c0614 100644 --- a/src/main/scala/app/PullRequestsController.scala +++ b/src/main/scala/app/PullRequestsController.scala @@ -105,9 +105,9 @@ } getOrElse NotFound }) - get("/:owner/:repository/pull/:id/delete/:branchName")(collaboratorsOnly { repository => + get("/:owner/:repository/pull/:id/delete/*")(collaboratorsOnly { repository => params("id").toIntOpt.map { issueId => - val branchName = params("branchName") + val branchName = multiParams("splat").head val userName = context.loginAccount.get.userName if(repository.repository.defaultBranch != branchName){ using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => @@ -228,16 +228,16 @@ val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2 val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2 - redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}") + redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}") } } getOrElse NotFound } case _ => { using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git => JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) => - redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}") + redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}") } getOrElse { - redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}") + redirect(s"/${forkedRepository.owner}/${forkedRepository.name}") } } } diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala index 6893e63..dda0e61 100644 --- a/src/main/scala/app/RepositoryViewerController.scala +++ b/src/main/scala/app/RepositoryViewerController.scala @@ -82,44 +82,45 @@ val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id)) @scala.annotation.tailrec - def getPathObjectId(path: String, walk: TreeWalk): ObjectId = walk.next match { - case true if(walk.getPathString == path) => walk.getObjectId(0) - case true => getPathObjectId(path, walk) + def getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match { + case true if(walk.getPathString == path) => Some(walk.getObjectId(0)) + case true => getPathObjectId(path, walk) + case false => None } - val objectId = using(new TreeWalk(git.getRepository)){ treeWalk => + using(new TreeWalk(git.getRepository)){ treeWalk => treeWalk.addTree(revCommit.getTree) treeWalk.setRecursive(true) getPathObjectId(path, treeWalk) - } - - if(raw){ - // Download - defining(JGitUtil.getContent(git, objectId, false).get){ bytes => - contentType = FileUtil.getContentType(path, bytes) - bytes - } - } else { - // Viewer - val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize) - val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other" - val bytes = if(viewer == "other") JGitUtil.getContent(git, objectId, false) else None - - val content = if(viewer == "other"){ - if(bytes.isDefined && FileUtil.isText(bytes.get)){ - // text - JGitUtil.ContentInfo("text", bytes.map(StringUtil.convertFromByteArray)) - } else { - // binary - JGitUtil.ContentInfo("binary", None) + } map { objectId => + if(raw){ + // Download + defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes => + contentType = FileUtil.getContentType(path, bytes) + bytes } } else { - // image or large - JGitUtil.ContentInfo(viewer, None) - } + // Viewer + val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize) + val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other" + val bytes = if(viewer == "other") JGitUtil.getContentFromId(git, objectId, false) else None - repo.html.blob(id, repository, path.split("/").toList, content, new JGitUtil.CommitInfo(revCommit)) - } + val content = if(viewer == "other"){ + if(bytes.isDefined && FileUtil.isText(bytes.get)){ + // text + JGitUtil.ContentInfo("text", bytes.map(StringUtil.convertFromByteArray)) + } else { + // binary + JGitUtil.ContentInfo("binary", None) + } + } else { + // image or large + JGitUtil.ContentInfo(viewer, None) + } + + repo.html.blob(id, repository, path.split("/").toList, content, new JGitUtil.CommitInfo(revCommit)) + } + } getOrElse NotFound } }) @@ -158,8 +159,8 @@ /** * Deletes branch. */ - get("/:owner/:repository/delete/:branchName")(collaboratorsOnly { repository => - val branchName = params("branchName") + get("/:owner/:repository/delete/*")(collaboratorsOnly { repository => + val branchName = multiParams("splat").head val userName = context.loginAccount.get.userName if(repository.repository.defaultBranch != branchName){ using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => @@ -180,8 +181,8 @@ /** * Download repository contents as an archive. */ - get("/:owner/:repository/archive/:name")(referrersOnly { repository => - val name = params("name") + get("/:owner/:repository/archive/*")(referrersOnly { repository => + val name = multiParams("splat").head if(name.endsWith(".zip")){ val revision = name.replaceFirst("\\.zip$", "") @@ -192,7 +193,7 @@ workDir.mkdirs val zipFile = new File(workDir, repository.name + "-" + - (if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip") + (if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + ".zip") using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision)) @@ -207,7 +208,7 @@ while(walk.next){ val name = walk.getPathString val mode = walk.getFileMode(0) - if(mode != FileMode.TREE){ + if(mode == FileMode.REGULAR_FILE || mode == FileMode.EXECUTABLE_FILE){ walk.getObjectId(objectId, 0) val entry = new ZipEntry(name) val loader = reader.open(objectId) @@ -266,21 +267,21 @@ repo.html.guide(repository) } else { using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head) + //val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head) // get specified commit - JGitUtil.getDefaultBranch(git, repository, revstr).map { - case (objectId, revision) => - defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit => - // get files - val files = JGitUtil.getFileList(git, revision, path) - val parentPath = if (path == ".") Nil else path.split("/").toList - // process README.md or README.markdown - val readme = files.find { file => - readmeFiles.contains(file.name.toLowerCase) - }.map { file => - val path = (file.name :: parentPath.reverse).reverse - path -> StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get) - } + JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) => + defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit => + // get files + val files = JGitUtil.getFileList(git, revision, path) + val parentPath = if (path == ".") Nil else path.split("/").toList + // process README.md or README.markdown + val readme = files.find { file => + readmeFiles.contains(file.name.toLowerCase) + }.map { file => + val path = (file.name :: parentPath.reverse).reverse + path -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId( + Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get) + } repo.html.files(revision, repository, if(path == ".") Nil else path.split("/").toList, // current path diff --git a/src/main/scala/app/SystemSettingsController.scala b/src/main/scala/app/SystemSettingsController.scala index c6f56fe..478c7c5 100644 --- a/src/main/scala/app/SystemSettingsController.scala +++ b/src/main/scala/app/SystemSettingsController.scala @@ -4,18 +4,21 @@ import SystemSettingsService._ import util.AdminAuthenticator import jp.sf.amateras.scalatra.forms._ +import ssh.SshServer class SystemSettingsController extends SystemSettingsControllerBase - with SystemSettingsService with AccountService with AdminAuthenticator + with AccountService with AdminAuthenticator trait SystemSettingsControllerBase extends ControllerBase { - self: SystemSettingsService with AccountService with AdminAuthenticator => + self: AccountService with AdminAuthenticator => private val form = mapping( "baseUrl" -> trim(label("Base URL", optional(text()))), "allowAccountRegistration" -> trim(label("Account registration", boolean())), "gravatar" -> trim(label("Gravatar", boolean())), "notification" -> trim(label("Notification", boolean())), + "ssh" -> trim(label("SSH access", boolean())), + "sshPort" -> trim(label("SSH port", optional(number()))), "smtp" -> optionalIfNotChecked("notification", mapping( "host" -> trim(label("SMTP Host", text(required))), "port" -> trim(label("SMTP Port", optional(number()))), @@ -38,15 +41,28 @@ "tls" -> trim(label("Enable TLS", optional(boolean()))), "keystore" -> trim(label("Keystore", optional(text()))) )(Ldap.apply)) - )(SystemSettings.apply) + )(SystemSettings.apply).verifying { settings => + if(settings.ssh && settings.baseUrl.isEmpty){ + Seq("baseUrl" -> "Base URL is required if SSH access is enabled.") + } else Nil + } get("/admin/system")(adminOnly { - admin.html.system(loadSystemSettings(), flash.get("info")) + admin.html.system(flash.get("info")) }) post("/admin/system", form)(adminOnly { form => saveSystemSettings(form) + + if(form.ssh && !SshServer.isActive && form.baseUrl.isDefined){ + SshServer.start(request.getServletContext, + form.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), + form.baseUrl.get) + } else if(!form.ssh && SshServer.isActive){ + SshServer.stop() + } + flash += "info" -> "System settings has been updated." redirect("/admin/system") }) diff --git a/src/main/scala/app/UserManagementController.scala b/src/main/scala/app/UserManagementController.scala index 50ffb3c..dc3543c 100644 --- a/src/main/scala/app/UserManagementController.scala +++ b/src/main/scala/app/UserManagementController.scala @@ -5,6 +5,7 @@ import util.StringUtil._ import util.ControlUtil._ import jp.sf.amateras.scalatra.forms._ +import org.scalatra.i18n.Messages import org.apache.commons.io.FileUtils import util.Directory._ @@ -23,10 +24,10 @@ fileId: Option[String], clearImage: Boolean, isRemoved: Boolean) case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], - memberNames: Option[String]) + members: String) case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], - memberNames: Option[String], clearImage: Boolean, isRemoved: Boolean) + members: String, clearImage: Boolean, isRemoved: Boolean) val newUserForm = mapping( "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))), @@ -51,28 +52,28 @@ )(EditUserForm.apply) val newGroupForm = mapping( - "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))), - "url" -> trim(label("URL" ,optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" ,optional(text()))), - "memberNames" -> trim(label("Member Names" ,optional(text()))) + "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "members" -> trim(label("Members" ,text(required, members))) )(NewGroupForm.apply) val editGroupForm = mapping( - "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))), - "url" -> trim(label("URL" ,optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" ,optional(text()))), - "memberNames" -> trim(label("Member Names" ,optional(text()))), - "clearImage" -> trim(label("Clear image" ,boolean())), - "removed" -> trim(label("Disable" ,boolean())) + "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "members" -> trim(label("Members" ,text(required, members))), + "clearImage" -> trim(label("Clear image" ,boolean())), + "removed" -> trim(label("Disable" ,boolean())) )(EditGroupForm.apply) get("/admin/users")(adminOnly { val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false) - val users = getAllUsers(includeRemoved) - - val members = users.collect { case account if(account.isGroupAccount) => - account.userName -> getGroupMembers(account.userName) + val users = getAllUsers(includeRemoved) + val members = users.collect { case account if(account.isGroupAccount) => + account.userName -> getGroupMembers(account.userName).map(_.userName) }.toMap + admin.users.html.list(users, members, includeRemoved) }) @@ -127,7 +128,11 @@ post("/admin/users/_newgroup", newGroupForm)(adminOnly { form => createGroup(form.groupName, form.url) - updateGroupMembers(form.groupName, form.memberNames.map(_.split(",").toList).getOrElse(Nil)) + updateGroupMembers(form.groupName, form.members.split(",").map { + _.split(":") match { + case Array(userName, isManager) => (userName, isManager.toBoolean) + } + }.toList) updateImage(form.groupName, form.fileId, false) redirect("/admin/users") }) @@ -139,7 +144,11 @@ }) post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form => - defining(params("groupName"), form.memberNames.map(_.split(",").toList).getOrElse(Nil)){ case (groupName, memberNames) => + defining(params("groupName"), form.members.split(",").map { + _.split(":") match { + case Array(userName, isManager) => (userName, isManager.toBoolean) + } + }.toList){ case (groupName, members) => getAccountByUserName(groupName, true).map { account => updateGroup(groupName, form.url, form.isRemoved) @@ -155,11 +164,11 @@ } } else { // Update GROUP_MEMBER - updateGroupMembers(form.groupName, memberNames) + updateGroupMembers(form.groupName, members) // Update COLLABORATOR for group repositories getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => removeCollaborators(form.groupName, repositoryName) - memberNames.foreach { userName => + members.foreach { case (userName, isManager) => addCollaborator(form.groupName, repositoryName, userName) } } @@ -172,8 +181,17 @@ } }) - post("/admin/users/_usercheck")(adminOnly { + // TODO Move to other generic controller? + post("/admin/users/_usercheck"){ getAccountByUserName(params("userName")).isDefined - }) + } + + private def members: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = { + if(value.split(",").exists { + _.split(":") match { case Array(userName, isManager) => isManager.toBoolean } + }) None else Some("Must select one manager at least.") + } + } } diff --git a/src/main/scala/model/GroupMembers.scala b/src/main/scala/model/GroupMembers.scala index 0bcd0af..a2a38f3 100644 --- a/src/main/scala/model/GroupMembers.scala +++ b/src/main/scala/model/GroupMembers.scala @@ -5,10 +5,12 @@ object GroupMembers extends Table[GroupMember]("GROUP_MEMBER") { def groupName = column[String]("GROUP_NAME", O PrimaryKey) def userName = column[String]("USER_NAME", O PrimaryKey) - def * = groupName ~ userName <> (GroupMember, GroupMember.unapply _) + def isManager = column[Boolean]("MANAGER") + def * = groupName ~ userName ~ isManager <> (GroupMember, GroupMember.unapply _) } case class GroupMember( groupName: String, - userName: String + userName: String, + isManager: Boolean ) \ No newline at end of file diff --git a/src/main/scala/model/SshKey.scala b/src/main/scala/model/SshKey.scala new file mode 100644 index 0000000..39d89d4 --- /dev/null +++ b/src/main/scala/model/SshKey.scala @@ -0,0 +1,22 @@ +package model + +import scala.slick.driver.H2Driver.simple._ + +object SshKeys extends Table[SshKey]("SSH_KEY") { + def userName = column[String]("USER_NAME") + def sshKeyId = column[Int]("SSH_KEY_ID", O AutoInc) + def title = column[String]("TITLE") + def publicKey = column[String]("PUBLIC_KEY") + + def ins = userName ~ title ~ publicKey returning sshKeyId + def * = userName ~ sshKeyId ~ title ~ publicKey <> (SshKey, SshKey.unapply _) + + def byPrimaryKey(userName: String, sshKeyId: Int) = (this.userName is userName.bind) && (this.sshKeyId is sshKeyId.bind) +} + +case class SshKey( + userName: String, + sshKeyId: Int, + title: String, + publicKey: String +) diff --git a/src/main/scala/service/AccountService.scala b/src/main/scala/service/AccountService.scala index ff1186f..c57d0ce 100644 --- a/src/main/scala/service/AccountService.scala +++ b/src/main/scala/service/AccountService.scala @@ -122,18 +122,17 @@ def updateGroup(groupName: String, url: Option[String], removed: Boolean): Unit = Accounts.filter(_.userName is groupName.bind).map(t => t.url.? ~ t.removed).update(url, removed) - def updateGroupMembers(groupName: String, members: List[String]): Unit = { + def updateGroupMembers(groupName: String, members: List[(String, Boolean)]): Unit = { Query(GroupMembers).filter(_.groupName is groupName.bind).delete - members.foreach { userName => - GroupMembers insert GroupMember (groupName, userName) + members.foreach { case (userName, isManager) => + GroupMembers insert GroupMember (groupName, userName, isManager) } } - def getGroupMembers(groupName: String): List[String] = + def getGroupMembers(groupName: String): List[GroupMember] = Query(GroupMembers) .filter(_.groupName is groupName.bind) .sortBy(_.userName) - .map(_.userName) .list def getGroupsByUserName(userName: String): List[String] = diff --git a/src/main/scala/service/IssuesService.scala b/src/main/scala/service/IssuesService.scala index b121599..590e300 100644 --- a/src/main/scala/service/IssuesService.scala +++ b/src/main/scala/service/IssuesService.scala @@ -8,7 +8,6 @@ import model._ import util.Implicits._ import util.StringUtil._ -import util.StringUtil trait IssuesService { import IssuesService._ @@ -120,16 +119,10 @@ // get issues and comment count and labels searchIssueQuery(repos, condition, filterUser, onlyPullRequest) .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } - .leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } - .leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) } - .map { case (((t1, t2), t3), t4) => - (t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?) - } - .sortBy(_._4) // labelName - .sortBy { case (t1, commentCount, _,_,_) => + .sortBy { case (t1, t2) => (condition.sort match { case "created" => t1.registeredDate - case "comments" => commentCount + case "comments" => t2.commentCount case "updated" => t1.updatedDate }) match { case sort => condition.direction match { @@ -139,6 +132,11 @@ } } .drop(offset).take(limit) + .leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } + .leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) } + .map { case (((t1, t2), t3), t4) => + (t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?) + } .list .splitWith { (c1, c2) => c1._1.userName == c2._1.userName && @@ -316,7 +314,7 @@ } def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String) = { - StringUtil.extractCloseId(message).foreach { issueId => + extractCloseId(message).foreach { issueId => for(issue <- getIssue(owner, repository, issueId) if !issue.closed){ createComment(owner, repository, userName, issue.issueId, "Close", "close") updateClosed(owner, repository, issue.issueId, true) diff --git a/src/main/scala/service/RepositorySearchService.scala b/src/main/scala/service/RepositorySearchService.scala index ac4f177..33ec942 100644 --- a/src/main/scala/service/RepositorySearchService.scala +++ b/src/main/scala/service/RepositorySearchService.scala @@ -63,8 +63,9 @@ val list = new ListBuffer[(String, String)] while (treeWalk.next()) { - if(treeWalk.getFileMode(0) != FileMode.TREE){ - JGitUtil.getContent(git, treeWalk.getObjectId(0), false).foreach { bytes => + val mode = treeWalk.getFileMode(0) + if(mode == FileMode.REGULAR_FILE || mode == FileMode.EXECUTABLE_FILE){ + JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).foreach { bytes => if(FileUtil.isText(bytes)){ val text = StringUtil.convertFromByteArray(bytes) val lowerText = text.toLowerCase diff --git a/src/main/scala/service/RepositoryService.scala b/src/main/scala/service/RepositoryService.scala index 74bd139..38e7606 100644 --- a/src/main/scala/service/RepositoryService.scala +++ b/src/main/scala/service/RepositoryService.scala @@ -147,7 +147,8 @@ getForkedCount( repository.originUserName.getOrElse(repository.userName), repository.originRepositoryName.getOrElse(repository.repositoryName) - )) + ), + getRepositoryManagers(repository.userName)) } } @@ -162,7 +163,8 @@ getForkedCount( repository.originUserName.getOrElse(repository.userName), repository.originRepositoryName.getOrElse(repository.repositoryName) - )) + ), + getRepositoryManagers(repository.userName)) } } @@ -195,10 +197,18 @@ getForkedCount( repository.originUserName.getOrElse(repository.userName), repository.originRepositoryName.getOrElse(repository.repositoryName) - )) + ), + getRepositoryManagers(repository.userName)) } } + private def getRepositoryManagers(userName: String): Seq[String] = + if(getAccountByUserName(userName).exists(_.isGroupAccount)){ + getGroupMembers(userName).collect { case x if(x.isManager) => x.userName } + } else { + Seq(userName) + } + /** * Updates the last activity date of the repository. */ @@ -278,21 +288,25 @@ object RepositoryService { - case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository, + case class RepositoryInfo(owner: String, name: String, httpUrl: String, repository: Repository, issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int, - branchList: List[String], tags: List[util.JGitUtil.TagInfo]){ + branchList: Seq[String], tags: Seq[util.JGitUtil.TagInfo], managers: Seq[String]){ + + lazy val host = """^https?://(.+?)(:\d+)?/""".r.findFirstMatchIn(httpUrl).get.group(1) + + def sshUrl(port: Int, userName: String) = s"ssh://${userName}@${host}:${port}/${owner}/${name}.git" /** * Creates instance with issue count and pull request count. */ - def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int) = - this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags) + def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, managers: Seq[String]) = + this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers) /** * Creates instance without issue count and pull request count. */ - def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int) = - this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags) + def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) = + this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers) } case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode]) diff --git a/src/main/scala/service/RequestCache.scala b/src/main/scala/service/RequestCache.scala index 0f0b0f0..3747e85 100644 --- a/src/main/scala/service/RequestCache.scala +++ b/src/main/scala/service/RequestCache.scala @@ -1,7 +1,6 @@ package service import model._ -import service.SystemSettingsService.SystemSettings /** * This service is used for a view helper mainly. @@ -9,28 +8,23 @@ * It may be called many times in one request, so each method stores * its result into the cache which available during a request. */ -trait RequestCache { - - def getSystemSettings()(implicit context: app.Context): SystemSettings = - context.cache("system_settings"){ - new SystemSettingsService {}.loadSystemSettings() - } +trait RequestCache extends SystemSettingsService with AccountService with IssuesService { def getIssue(userName: String, repositoryName: String, issueId: String)(implicit context: app.Context): Option[Issue] = { context.cache(s"issue.${userName}/${repositoryName}#${issueId}"){ - new IssuesService {}.getIssue(userName, repositoryName, issueId) + super.getIssue(userName, repositoryName, issueId) } } def getAccountByUserName(userName: String)(implicit context: app.Context): Option[Account] = { context.cache(s"account.${userName}"){ - new AccountService {}.getAccountByUserName(userName) + super.getAccountByUserName(userName) } } def getAccountByMailAddress(mailAddress: String)(implicit context: app.Context): Option[Account] = { context.cache(s"account.${mailAddress}"){ - new AccountService {}.getAccountByMailAddress(mailAddress) + super.getAccountByMailAddress(mailAddress) } } } diff --git a/src/main/scala/service/SshKeyService.scala b/src/main/scala/service/SshKeyService.scala new file mode 100644 index 0000000..c249a3a --- /dev/null +++ b/src/main/scala/service/SshKeyService.scala @@ -0,0 +1,19 @@ +package service + +import model._ +import scala.slick.driver.H2Driver.simple._ +import Database.threadLocalSession + +trait SshKeyService { + + def addPublicKey(userName: String, title: String, publicKey: String): Unit = + SshKeys.ins insert (userName, title, publicKey) + + def getPublicKeys(userName: String): List[SshKey] = + Query(SshKeys).filter(_.userName is userName.bind).sortBy(_.sshKeyId).list + + def deletePublicKey(userName: String, sshKeyId: Int): Unit = + SshKeys filter (_.byPrimaryKey(userName, sshKeyId)) delete + + +} diff --git a/src/main/scala/service/SystemSettingsService.scala b/src/main/scala/service/SystemSettingsService.scala index f15bd37..ba425a3 100644 --- a/src/main/scala/service/SystemSettingsService.scala +++ b/src/main/scala/service/SystemSettingsService.scala @@ -15,10 +15,12 @@ def saveSystemSettings(settings: SystemSettings): Unit = { defining(new java.util.Properties()){ props => - settings.baseUrl.foreach(props.setProperty(BaseURL, _)) + settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", ""))) props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString) props.setProperty(Gravatar, settings.gravatar.toString) props.setProperty(Notification, settings.notification.toString) + props.setProperty(Ssh, settings.ssh.toString) + settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString)) if(settings.notification) { settings.smtp.foreach { smtp => props.setProperty(SmtpHost, smtp.host) @@ -56,10 +58,12 @@ props.load(new java.io.FileInputStream(GitBucketConf)) } SystemSettings( - getOptionValue(props, BaseURL, None), + getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")), getValue(props, AllowAccountRegistration, false), getValue(props, Gravatar, true), getValue(props, Notification, false), + getValue(props, Ssh, false), + getOptionValue(props, SshPort, Some(DefaultSshPort)), if(getValue(props, Notification, false)){ Some(Smtp( getValue(props, SmtpHost, ""), @@ -102,6 +106,8 @@ allowAccountRegistration: Boolean, gravatar: Boolean, notification: Boolean, + ssh: Boolean, + sshPort: Option[Int], smtp: Option[Smtp], ldapAuthentication: Boolean, ldap: Option[Ldap]) @@ -127,6 +133,7 @@ fromAddress: Option[String], fromName: Option[String]) + val DefaultSshPort = 29418 val DefaultSmtpPort = 25 val DefaultLdapPort = 389 @@ -134,6 +141,8 @@ 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" diff --git a/src/main/scala/service/WebHookService.scala b/src/main/scala/service/WebHookService.scala index 496f784..5662c44 100644 --- a/src/main/scala/service/WebHookService.scala +++ b/src/main/scala/service/WebHookService.scala @@ -87,7 +87,7 @@ refName, commits.map { commit => val diffs = JGitUtil.getDiffs(git, commit.id, false) - val commitUrl = repositoryInfo.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/commit/" + commit.id + val commitUrl = repositoryInfo.httpUrl.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/commit/" + commit.id WebHookCommit( id = commit.id, @@ -106,7 +106,7 @@ }.toList, WebHookRepository( name = repositoryInfo.name, - url = repositoryInfo.url, + url = repositoryInfo.httpUrl, description = repositoryInfo.repository.description.getOrElse(""), watchers = 0, forks = repositoryInfo.forkedCount, diff --git a/src/main/scala/service/WikiService.scala b/src/main/scala/service/WikiService.scala index 00fbabd..870fe20 100644 --- a/src/main/scala/service/WikiService.scala +++ b/src/main/scala/service/WikiService.scala @@ -15,6 +15,7 @@ import org.eclipse.jgit.api.errors.PatchFormatException import scala.collection.JavaConverters._ import scala.Some +import service.RepositoryService.RepositoryInfo object WikiService { @@ -40,6 +41,10 @@ */ case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date) + def httpUrl(repository: RepositoryInfo) = repository.httpUrl.replaceFirst("\\.git\\Z", ".wiki.git") + + def sshUrl(repository: RepositoryInfo, settings: SystemSettingsService.SystemSettings, userName: String) = + repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), userName).replaceFirst("\\.git\\Z", ".wiki.git") } trait WikiService { @@ -234,7 +239,7 @@ builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) } else { created = false - updated = JGitUtil.getContent(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false) + updated = JGitUtil.getContentFromId(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false) } } } @@ -268,35 +273,35 @@ */ def deleteWikiPage(owner: String, repository: String, pageName: String, committer: String, mailAddress: String, message: String): Unit = { - LockUtil.lock(s"${owner}/${repository}/wiki"){ - using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => - val builder = DirCache.newInCore.builder() - val inserter = git.getRepository.newObjectInserter() - val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") - var removed = false + LockUtil.lock(s"${owner}/${repository}/wiki"){ + using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => + val builder = DirCache.newInCore.builder() + val inserter = git.getRepository.newObjectInserter() + val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") + var removed = false - using(new RevWalk(git.getRepository)){ revWalk => - using(new TreeWalk(git.getRepository)){ treeWalk => - val index = treeWalk.addTree(revWalk.parseTree(headId)) - treeWalk.setRecursive(true) - while(treeWalk.next){ - val path = treeWalk.getPathString - val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser]) - if(path != pageName + ".md"){ - builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) - } else { - removed = true - } + using(new RevWalk(git.getRepository)){ revWalk => + using(new TreeWalk(git.getRepository)){ treeWalk => + val index = treeWalk.addTree(revWalk.parseTree(headId)) + treeWalk.setRecursive(true) + while(treeWalk.next){ + val path = treeWalk.getPathString + val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser]) + if(path != pageName + ".md"){ + builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) + } else { + removed = true } } + } - if(removed){ - builder.finish() - JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message) - } + if(removed){ + builder.finish() + JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message) } } } + } } } diff --git a/src/main/scala/servlet/AutoUpdateListener.scala b/src/main/scala/servlet/AutoUpdateListener.scala index bfa6992..8cafb60 100644 --- a/src/main/scala/servlet/AutoUpdateListener.scala +++ b/src/main/scala/servlet/AutoUpdateListener.scala @@ -50,6 +50,7 @@ * The history of versions. A head of this sequence is the current BitBucket version. */ val versions = Seq( + Version(1, 12), Version(1, 11), Version(1, 10), Version(1, 9), @@ -97,7 +98,7 @@ */ def getCurrentVersion(): Version = { if(versionFile.exists){ - FileUtils.readFileToString(versionFile, "UTF-8").split("\\.") match { + FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match { case Array(majorVersion, minorVersion) => { versions.find { v => v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt diff --git a/src/main/scala/servlet/GitRepositoryServlet.scala b/src/main/scala/servlet/GitRepositoryServlet.scala index 9efefd3..60a5224 100644 --- a/src/main/scala/servlet/GitRepositoryServlet.scala +++ b/src/main/scala/servlet/GitRepositoryServlet.scala @@ -8,7 +8,7 @@ import javax.servlet.ServletConfig import javax.servlet.ServletContext -import javax.servlet.http.HttpServletRequest +import javax.servlet.http.{HttpServletResponse, HttpServletRequest} import util.{StringUtil, Keys, JGitUtil, Directory} import util.ControlUtil._ import util.Implicits._ @@ -23,7 +23,7 @@ * This servlet provides only Git repository functionality. * Authentication is provided by [[servlet.BasicAuthenticationFilter]]. */ -class GitRepositoryServlet extends GitServlet { +class GitRepositoryServlet extends GitServlet with SystemSettingsService { private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet]) @@ -47,7 +47,19 @@ super.init(config) } - + + override def service(req: HttpServletRequest, res: HttpServletResponse): Unit = { + val agent = req.getHeader("USER-AGENT") + val index = req.getRequestURI.indexOf(".git") + if(index >= 0 && (agent == null || agent.toLowerCase.indexOf("git/") < 0)){ + // redirect for browsers + val paths = req.getRequestURI.substring(0, index).split("/") + res.sendRedirect(baseUrl(req) + "/" + paths.dropRight(1).last + "/" + paths.last) + } else { + // response for git client + super.service(req, res) + } + } } class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService { @@ -87,12 +99,16 @@ using(Git.open(Directory.getRepositoryDir(owner, repository))) { git => commands.asScala.foreach { command => logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}") - val commits = command.getType match { - case ReceiveCommand.Type.DELETE => Nil - case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name) - } val refName = command.getRefName.split("/") val branchName = refName.drop(2).mkString("/") + val commits = if (refName(1) == "tags") { + Nil + } else { + command.getType match { + case ReceiveCommand.Type.DELETE => Nil + case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name) + } + } // Extract new commit and apply issue comment val newCommits = if(commits.size > 1000){ diff --git a/src/main/scala/ssh/GitCommand.scala b/src/main/scala/ssh/GitCommand.scala new file mode 100644 index 0000000..9f014f0 --- /dev/null +++ b/src/main/scala/ssh/GitCommand.scala @@ -0,0 +1,130 @@ +package ssh + +import org.apache.sshd.server.{CommandFactory, Environment, ExitCallback, Command} +import org.slf4j.LoggerFactory +import java.io.{InputStream, OutputStream} +import util.ControlUtil._ +import org.eclipse.jgit.api.Git +import util.Directory._ +import org.eclipse.jgit.transport.{ReceivePack, UploadPack} +import org.apache.sshd.server.command.UnknownCommand +import servlet.{Database, CommitLogHook} +import service.{AccountService, RepositoryService, SystemSettingsService} +import org.eclipse.jgit.errors.RepositoryNotFoundException +import javax.servlet.ServletContext + + +object GitCommand { + val CommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+).git'\Z""".r +} + +abstract class GitCommand(val context: ServletContext, val owner: String, val repoName: String) extends Command { + self: RepositoryService with AccountService => + + private val logger = LoggerFactory.getLogger(classOf[GitCommand]) + protected var err: OutputStream = null + protected var in: InputStream = null + protected var out: OutputStream = null + protected var callback: ExitCallback = null + + protected def runTask(user: String): Unit + + private def newTask(user: String): Runnable = new Runnable { + override def run(): Unit = { + Database(context) withTransaction { + try { + runTask(user) + callback.onExit(0) + } catch { + case e: RepositoryNotFoundException => + logger.info(e.getMessage) + callback.onExit(1, "Repository Not Found") + case e: Throwable => + logger.error(e.getMessage, e) + callback.onExit(1) + } + } + } + } + + override def start(env: Environment): Unit = { + val user = env.getEnv.get("USER") + val thread = new Thread(newTask(user)) + thread.start() + } + + override def destroy(): Unit = {} + + override def setExitCallback(callback: ExitCallback): Unit = { + this.callback = callback + } + + override def setErrorStream(err: OutputStream): Unit = { + this.err = err + } + + override def setOutputStream(out: OutputStream): Unit = { + this.out = out + } + + override def setInputStream(in: InputStream): Unit = { + this.in = in + } + + protected def isWritableUser(username: String, repositoryInfo: RepositoryService.RepositoryInfo): Boolean = + getAccountByUserName(username) match { + case Some(account) => hasWritePermission(repositoryInfo.owner, repositoryInfo.name, Some(account)) + case None => false + } + +} + +class GitUploadPack(context: ServletContext, owner: String, repoName: String, baseUrl: String) extends GitCommand(context, owner, repoName) + with RepositoryService with AccountService { + + override protected def runTask(user: String): Unit = { + getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo => + if(!repositoryInfo.repository.isPrivate || isWritableUser(user, repositoryInfo)){ + using(Git.open(getRepositoryDir(owner, repoName))) { git => + val repository = git.getRepository + val upload = new UploadPack(repository) + upload.upload(in, out, err) + } + } + } + } + +} + +class GitReceivePack(context: ServletContext, owner: String, repoName: String, baseUrl: String) extends GitCommand(context, owner, repoName) + with SystemSettingsService with RepositoryService with AccountService { + + override protected def runTask(user: String): Unit = { + getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo => + if(isWritableUser(user, repositoryInfo)){ + using(Git.open(getRepositoryDir(owner, repoName))) { git => + val repository = git.getRepository + val receive = new ReceivePack(repository) + if(!repoName.endsWith(".wiki")){ + receive.setPostReceiveHook(new CommitLogHook(owner, repoName, user, baseUrl)) + } + receive.receive(in, out, err) + } + } + } + } + +} + +class GitCommandFactory(context: ServletContext, baseUrl: String) extends CommandFactory { + private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory]) + + override def createCommand(command: String): Command = { + logger.debug(s"command: $command") + command match { + case GitCommand.CommandRegex("upload", owner, repoName) => new GitUploadPack(context, owner, repoName, baseUrl) + case GitCommand.CommandRegex("receive", owner, repoName) => new GitReceivePack(context, owner, repoName, baseUrl) + case _ => new UnknownCommand(command) + } + } +} \ No newline at end of file diff --git a/src/main/scala/ssh/NoShell.scala b/src/main/scala/ssh/NoShell.scala new file mode 100644 index 0000000..c107be6 --- /dev/null +++ b/src/main/scala/ssh/NoShell.scala @@ -0,0 +1,62 @@ +package ssh + +import org.apache.sshd.common.Factory +import org.apache.sshd.server.{Environment, ExitCallback, Command} +import java.io.{OutputStream, InputStream} +import org.eclipse.jgit.lib.Constants +import service.SystemSettingsService + +class NoShell extends Factory[Command] with SystemSettingsService { + override def create(): Command = new Command() { + private var in: InputStream = null + private var out: OutputStream = null + private var err: OutputStream = null + private var callback: ExitCallback = null + + override def start(env: Environment): Unit = { + val user = env.getEnv.get("USER") + val port = loadSystemSettings().sshPort.getOrElse(SystemSettingsService.DefaultSshPort) + val message = + """ + | Welcome to + | _____ _ _ ____ _ _ + | / ____| (_) | | | _ \ | | | | + | | | __ _ | |_ | |_) | _ _ ___ | | __ ___ | |_ + | | | |_ | | | | __| | _ < | | | | / __| | |/ / / _ \ | __| + | | |__| | | | | |_ | |_) | | |_| | | (__ | < | __/ | |_ + | \_____| |_| \__| |____/ \__,_| \___| |_|\_\ \___| \__| + | + | Successfully SSH Access. + | But interactive shell is disabled. + | + | Please use: + | + | git clone ssh://%s@GITBUCKET_HOST:%d/OWNER/REPOSITORY_NAME.git + """.stripMargin.format(user, port).replace("\n", "\r\n") + "\r\n" + err.write(Constants.encode(message)) + err.flush() + in.close() + out.close() + err.close() + callback.onExit(127) + } + + override def destroy(): Unit = {} + + override def setInputStream(in: InputStream): Unit = { + this.in = in + } + + override def setOutputStream(out: OutputStream): Unit = { + this.out = out + } + + override def setErrorStream(err: OutputStream): Unit = { + this.err = err + } + + override def setExitCallback(callback: ExitCallback): Unit = { + this.callback = callback + } + } +} diff --git a/src/main/scala/ssh/PublicKeyAuthenticator.scala b/src/main/scala/ssh/PublicKeyAuthenticator.scala new file mode 100644 index 0000000..a9ea634 --- /dev/null +++ b/src/main/scala/ssh/PublicKeyAuthenticator.scala @@ -0,0 +1,23 @@ +package ssh + +import org.apache.sshd.server.PublickeyAuthenticator +import org.apache.sshd.server.session.ServerSession +import java.security.PublicKey +import service.SshKeyService +import servlet.Database +import javax.servlet.ServletContext + +class PublicKeyAuthenticator(context: ServletContext) extends PublickeyAuthenticator with SshKeyService { + + override def authenticate(username: String, key: PublicKey, session: ServerSession): Boolean = { + Database(context) withTransaction { + getPublicKeys(username).exists { sshKey => + SshUtil.str2PublicKey(sshKey.publicKey) match { + case Some(publicKey) => key.equals(publicKey) + case _ => false + } + } + } + } + +} diff --git a/src/main/scala/ssh/SshServerListener.scala b/src/main/scala/ssh/SshServerListener.scala new file mode 100644 index 0000000..f8e08fd --- /dev/null +++ b/src/main/scala/ssh/SshServerListener.scala @@ -0,0 +1,68 @@ +package ssh + +import javax.servlet.{ServletContext, ServletContextEvent, ServletContextListener} +import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider +import org.slf4j.LoggerFactory +import util.Directory +import service.SystemSettingsService +import java.util.concurrent.atomic.AtomicBoolean + +object SshServer { + private val logger = LoggerFactory.getLogger(SshServer.getClass) + private val server = org.apache.sshd.SshServer.setUpDefaultServer() + private val active = new AtomicBoolean(false) + + private def configure(context: ServletContext, port: Int, baseUrl: String) = { + server.setPort(port) + server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(s"${Directory.GitBucketHome}/gitbucket.ser")) + server.setPublickeyAuthenticator(new PublicKeyAuthenticator(context)) + server.setCommandFactory(new GitCommandFactory(context, baseUrl)) + server.setShellFactory(new NoShell) + } + + def start(context: ServletContext, port: Int, baseUrl: String) = { + if(active.compareAndSet(false, true)){ + configure(context, port, baseUrl) + server.start() + logger.info(s"Start SSH Server Listen on ${server.getPort}") + } + } + + def stop() = { + if(active.compareAndSet(true, false)){ + server.stop(true) + } + } + + def isActive = active.get +} + +/* + * Start a SSH Server Daemon + * + * How to use: + * git clone ssh://username@host_or_ip:29418/owner/repository_name.git + */ +class SshServerListener extends ServletContextListener with SystemSettingsService { + + override def contextInitialized(sce: ServletContextEvent): Unit = { + val settings = loadSystemSettings() + if(settings.ssh){ + if(settings.baseUrl.isEmpty){ + // TODO use logger? + println("Could not start SshServer because the baseUrl is not configured.") + } else { + SshServer.start(sce.getServletContext, + settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), + settings.baseUrl.get) + } + } + } + + override def contextDestroyed(sce: ServletContextEvent): Unit = { + if(loadSystemSettings().ssh){ + SshServer.stop() + } + } + +} diff --git a/src/main/scala/ssh/SshUtil.scala b/src/main/scala/ssh/SshUtil.scala new file mode 100644 index 0000000..db578de --- /dev/null +++ b/src/main/scala/ssh/SshUtil.scala @@ -0,0 +1,33 @@ +package ssh + +import java.security.PublicKey +import org.slf4j.LoggerFactory +import org.apache.commons.codec.binary.Base64 +import org.eclipse.jgit.lib.Constants +import org.apache.sshd.common.util.{KeyUtils, Buffer} + +object SshUtil { + + private val logger = LoggerFactory.getLogger(SshUtil.getClass) + + def str2PublicKey(key: String): Option[PublicKey] = { + // TODO RFC 4716 Public Key is not supported... + val parts = key.split(" ") + if (parts.size < 2) { + logger.debug(s"Invalid PublicKey Format: key") + return None + } + try { + val encodedKey = parts(1) + val decode = Base64.decodeBase64(Constants.encodeASCII(encodedKey)) + Some(new Buffer(decode).getRawPublicKey) + } catch { + case e: Throwable => + logger.debug(e.getMessage, e) + None + } + } + + def fingerPrint(key: String): String = KeyUtils.getFingerPrint(str2PublicKey(key).get) + +} diff --git a/src/main/scala/util/Authenticator.scala b/src/main/scala/util/Authenticator.scala index c524713..f40af7b 100644 --- a/src/main/scala/util/Authenticator.scala +++ b/src/main/scala/util/Authenticator.scala @@ -29,7 +29,7 @@ /** * Allows only the repository owner and administrators. */ -trait OwnerAuthenticator { self: ControllerBase with RepositoryService => +trait OwnerAuthenticator { self: ControllerBase with RepositoryService with AccountService => protected def ownerOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } protected def ownerOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } @@ -40,6 +40,9 @@ context.loginAccount match { case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(repository.owner == x.userName) => action(repository) + case Some(x) if(getGroupMembers(repository.owner).exists { member => + member.userName == x.userName && member.isManager == true + }) => action(repository) case _ => Unauthorized() } } getOrElse NotFound() @@ -106,7 +109,7 @@ } /** - * Allows only the repository owner and administrators. + * Allows only the repository owner (or manager for group repository) and administrators. */ trait ReferrerAuthenticator { self: ControllerBase with RepositoryService => protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } @@ -155,3 +158,24 @@ } } } + +/** + * 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() + } + } + } + } +} diff --git a/src/main/scala/util/JGitUtil.scala b/src/main/scala/util/JGitUtil.scala index 57fd421..5ab4bf7 100644 --- a/src/main/scala/util/JGitUtil.scala +++ b/src/main/scala/util/JGitUtil.scala @@ -11,17 +11,20 @@ import org.eclipse.jgit.treewalk._ import org.eclipse.jgit.treewalk.filter._ import org.eclipse.jgit.diff.DiffEntry.ChangeType -import org.eclipse.jgit.errors.MissingObjectException +import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException} import java.util.Date import org.eclipse.jgit.api.errors.NoHeadException import service.RepositoryService import org.eclipse.jgit.dircache.DirCacheEntry +import org.slf4j.LoggerFactory /** * Provides complex JGit operations. */ object JGitUtil { + private val logger = LoggerFactory.getLogger(JGitUtil.getClass) + /** * The repository data. * @@ -45,9 +48,10 @@ * @param commitId the last commit id * @param committer the last committer name * @param mailAddress the committer's mail address + * @param linkUrl the url of submodule */ case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, time: Date, message: String, commitId: String, - committer: String, mailAddress: String) + committer: String, mailAddress: String, linkUrl: Option[String]) /** * The commit data. @@ -72,11 +76,7 @@ rev.getFullMessage, rev.getParents().map(_.name).toList) - val summary = defining(fullMessage.trim.indexOf("\n")){ i => - defining(if(i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage){ firstLine => - if(firstLine.length > shortMessage.length) shortMessage else firstLine - } - } + val summary = getSummaryMessage(fullMessage, shortMessage) val description = defining(fullMessage.trim.indexOf("\n")){ i => if(i >= 0){ @@ -105,6 +105,15 @@ 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. * * @param git the Git object @@ -152,7 +161,7 @@ } } } - + /** * Returns the file list of the specified path. * @@ -162,7 +171,7 @@ * @return HTML of the file list */ def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = { - val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String)] + val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])] using(new RevWalk(git.getRepository)){ revWalk => val objectId = git.getRepository.resolve(revision) @@ -195,22 +204,30 @@ }) } while (treeWalk.next()) { - list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString)) + // submodule + val linkUrl = if(treeWalk.getFileMode(0) == FileMode.GITLINK){ + getSubmodules(git, revCommit.getTree).find(_.path == treeWalk.getPathString).map(_.url) + } else None + + list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl)) } } } val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision) - list.map { case (objectId, fileMode, path, name) => - FileInfo( - objectId, - fileMode == FileMode.TREE, - name, - commits(path).getCommitterIdent.getWhen, - commits(path).getShortMessage, - commits(path).getName, - commits(path).getCommitterIdent.getName, - commits(path).getCommitterIdent.getEmailAddress) + list.map { case (objectId, fileMode, path, name, linkUrl) => + defining(commits(path)){ commit => + FileInfo( + objectId, + fileMode == FileMode.TREE || fileMode == FileMode.GITLINK, + name, + commit.getCommitterIdent.getWhen, + getSummaryMessage(commit.getFullMessage, commit.getShortMessage), + commit.getName, + commit.getCommitterIdent.getName, + commit.getCommitterIdent.getEmailAddress, + linkUrl) + } }.sortWith { (file1, file2) => (file1.isDirectory, file2.isDirectory) match { case (true , false) => true @@ -219,7 +236,18 @@ } }.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. * @@ -326,27 +354,6 @@ } /** - * 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. */ def getDiffs(git: Git, id: String, fetchContent: Boolean = true): (List[DiffInfo], Option[String]) = { @@ -377,7 +384,7 @@ DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None) } else { DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, - JGitUtil.getContent(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray)) + JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray)) })) } (buffer.toList, None) @@ -400,8 +407,8 @@ DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None) } else { DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, - JGitUtil.getContent(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray), - JGitUtil.getContent(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray)) + JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray), + JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray)) } }.toList } @@ -494,4 +501,73 @@ newHeadId.getName } + /** + * Read submodule information from .gitmodules + */ + def getSubmodules(git: Git, tree: RevTree): List[SubmoduleInfo] = { + val repository = git.getRepository + getContentFromPath(git, tree, ".gitmodules", true).map { bytes => + (try { + val config = new BlobBasedConfig(repository.getConfig(), bytes) + config.getSubsections("submodule").asScala.map { module => + val path = config.getString("submodule", module, "path") + val url = config.getString("submodule", module, "url") + SubmoduleInfo(module, path, url) + } + } catch { + case e: ConfigInvalidException => { + logger.error("Failed to load .gitmodules file for " + repository.getDirectory(), e) + Nil + } + }).toList + } getOrElse Nil + } + + /** + * Get object content of the given path as byte array from the Git repository. + * + * @param git the Git object + * @param revTree the rev tree + * @param path the path + * @param fetchLargeFile if false then returns None for the large file + * @return the byte array of content or None if object does not exist + */ + def getContentFromPath(git: Git, revTree: RevTree, path: String, fetchLargeFile: Boolean): Option[Array[Byte]] = { + @scala.annotation.tailrec + def getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match { + case true if(walk.getPathString == path) => Some(walk.getObjectId(0)) + case true => getPathObjectId(path, walk) + case false => None + } + + using(new TreeWalk(git.getRepository)){ treeWalk => + treeWalk.addTree(revTree) + treeWalk.setRecursive(true) + getPathObjectId(path, treeWalk) + } flatMap { objectId => + getContentFromId(git, objectId, fetchLargeFile) + } + } + + /** + * Get object content of the given object id as byte array from the Git repository. + * + * @param git the Git object + * @param id the object id + * @param fetchLargeFile if false then returns None for the large file + * @return the byte array of content or None if object does not exist + */ + def getContentFromId(git: Git, id: ObjectId, fetchLargeFile: Boolean): Option[Array[Byte]] = try { + val loader = git.getRepository.getObjectDatabase.open(id) + if(fetchLargeFile == false && FileUtil.isLarge(loader.getSize)){ + None + } else { + using(git.getRepository.getObjectDatabase){ db => + Some(db.open(id).getBytes) + } + } + } catch { + case e: MissingObjectException => None + } + } diff --git a/src/main/scala/view/AvatarImageProvider.scala b/src/main/scala/view/AvatarImageProvider.scala index bcc2d6b..1dff9ab 100644 --- a/src/main/scala/view/AvatarImageProvider.scala +++ b/src/main/scala/view/AvatarImageProvider.scala @@ -16,7 +16,7 @@ val src = if(mailAddress.isEmpty){ // by user name getAccountByUserName(userName).map { account => - if(account.image.isEmpty && getSystemSettings().gravatar){ + if(account.image.isEmpty && context.settings.gravatar){ s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}""" } else { s"""${context.path}/${account.userName}/_avatar""" @@ -27,13 +27,13 @@ } else { // by mail address getAccountByMailAddress(mailAddress).map { account => - if(account.image.isEmpty && getSystemSettings().gravatar){ + if(account.image.isEmpty && context.settings.gravatar){ s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}""" } else { s"""${context.path}/${account.userName}/_avatar""" } } getOrElse { - if(getSystemSettings().gravatar){ + if(context.settings.gravatar){ s"""https://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}""" } else { s"""${context.path}/_unknown/_avatar""" diff --git a/src/main/scala/view/Markdown.scala b/src/main/scala/view/Markdown.scala index 6b7ab2d..4d998bd 100644 --- a/src/main/scala/view/Markdown.scala +++ b/src/main/scala/view/Markdown.scala @@ -45,7 +45,7 @@ (text, text) } - val url = repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/" + StringUtil.urlEncode(page) + val url = repository.httpUrl.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/" + StringUtil.urlEncode(page) if(getWikiPage(repository.owner, repository.name, page).isDefined){ new Rendering(url, label) @@ -104,7 +104,7 @@ if(!enableWikiLink || url.startsWith("http://") || url.startsWith("https://")){ url } else { - repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/_blob/" + url + repository.httpUrl.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/_blob/" + url } } diff --git a/src/main/twirl/account/edit.scala.html b/src/main/twirl/account/edit.scala.html index 5213ddc..c02bcbe 100644 --- a/src/main/twirl/account/edit.scala.html +++ b/src/main/twirl/account/edit.scala.html @@ -1,71 +1,62 @@ -@(account: Option[model.Account], info: Option[Any])(implicit context: app.Context) +@(account: model.Account, info: Option[Any])(implicit context: app.Context) @import context._ @import view.helpers._ -@html.main((if(account.isDefined) "Edit your profile" else "Create your account")){ - @if(account.isDefined){ -

Edit your profile

- } else { -

Create your account

- } - @helper.html.information(info) -
-
-
- @if(account.isEmpty){ -
- - - -
- } - @if(account.map(_.password.nonEmpty).getOrElse(true)){ -
- - - -
- } -
- - - -
-
- - - -
-
- - - -
-
-
-
- - @helper.html.uploadavatar(account) -
+@html.main("Edit your profile"){ +
+
+ @menu("profile", settings.ssh) +
+
+ @helper.html.information(info) + +
+
Profile
+
+
+
+ @if(account.password.nonEmpty){ +
+ + + +
+ } +
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+ + @helper.html.uploadavatar(Some(account)) +
+
+
+
+ + + Cancel +
-
- @if(account.isDefined){ - - - Cancel - } else { - - } -
+
} \ No newline at end of file diff --git a/src/main/twirl/account/main.scala.html b/src/main/twirl/account/main.scala.html index 0b6cc6e..17e05c0 100644 --- a/src/main/twirl/account/main.scala.html +++ b/src/main/twirl/account/main.scala.html @@ -1,4 +1,5 @@ -@(account: model.Account, groupNames: List[String], active: String)(body: Html)(implicit context: app.Context) +@(account: model.Account, groupNames: List[String], active: String, + isGroupManager: Boolean = false)(body: Html)(implicit context: app.Context) @import context._ @import view.helpers._ @html.main(account.userName){ @@ -41,6 +42,13 @@
} + @if(loginAccount.isDefined && account.isGroupAccount && isGroupManager){ +
  • + +
  • + } @body
    diff --git a/src/main/twirl/account/members.scala.html b/src/main/twirl/account/members.scala.html index 14d7c77..0e21d01 100644 --- a/src/main/twirl/account/members.scala.html +++ b/src/main/twirl/account/members.scala.html @@ -1,7 +1,7 @@ -@(account: model.Account, members: List[String])(implicit context: app.Context) +@(account: model.Account, members: List[String], isGroupManager: Boolean)(implicit context: app.Context) @import context._ @import view.helpers._ -@main(account, Nil, "members"){ +@main(account, Nil, "members", isGroupManager){ @if(members.isEmpty){ No members } else { diff --git a/src/main/twirl/account/menu.scala.html b/src/main/twirl/account/menu.scala.html new file mode 100644 index 0000000..a5d9139 --- /dev/null +++ b/src/main/twirl/account/menu.scala.html @@ -0,0 +1,14 @@ +@(active: String, ssh: Boolean)(implicit context: app.Context) +@import context._ +
    + +
    diff --git a/src/main/twirl/account/newrepo.scala.html b/src/main/twirl/account/newrepo.scala.html new file mode 100644 index 0000000..b5f6c6a --- /dev/null +++ b/src/main/twirl/account/newrepo.scala.html @@ -0,0 +1,73 @@ +@(groupNames: List[String])(implicit context: app.Context) +@import context._ +@import view.helpers._ +@html.main("Create a New Repository"){ +
    +
    +
    + +
    + + + +
    + / + + +
    +
    + + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +} + diff --git a/src/main/twirl/account/register.scala.html b/src/main/twirl/account/register.scala.html new file mode 100644 index 0000000..7bce2db --- /dev/null +++ b/src/main/twirl/account/register.scala.html @@ -0,0 +1,48 @@ +@()(implicit context: app.Context) +@import context._ +@import view.helpers._ +@html.main("Create your account"){ +

    Create your account

    +
    +
    +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    +
    +
    +
    + + @helper.html.uploadavatar(None) +
    +
    +
    +
    + +
    +
    +} diff --git a/src/main/twirl/account/repositories.scala.html b/src/main/twirl/account/repositories.scala.html index f9037f5..100c2b4 100644 --- a/src/main/twirl/account/repositories.scala.html +++ b/src/main/twirl/account/repositories.scala.html @@ -1,7 +1,9 @@ -@(account: model.Account, groupNames: List[String], repositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context) +@(account: model.Account, groupNames: List[String], + repositories: List[service.RepositoryService.RepositoryInfo], + isGroupManager: Boolean)(implicit context: app.Context) @import context._ @import view.helpers._ -@main(account, groupNames, "repositories"){ +@main(account, groupNames, "repositories", isGroupManager){ @if(repositories.isEmpty){ No repositories } else { diff --git a/src/main/twirl/account/ssh.scala.html b/src/main/twirl/account/ssh.scala.html new file mode 100644 index 0000000..e6ee34c --- /dev/null +++ b/src/main/twirl/account/ssh.scala.html @@ -0,0 +1,45 @@ +@(account: model.Account, sshKeys: List[model.SshKey])(implicit context: app.Context) +@import context._ +@import view.helpers._ +@html.main("SSH Keys"){ +
    +
    + @menu("ssh", settings.ssh) +
    +
    +
    +
    SSH Keys
    +
    + @if(sshKeys.isEmpty){ + No keys + } + @sshKeys.zipWithIndex.map { case (key, i) => + @if(i != 0){ +
    + } + @key.title (@_root_.ssh.SshUtil.fingerPrint(key.publicKey)) + Delete + } +
    +
    +
    +
    +
    Add an SSH Key
    +
    +
    + +
    + +
    +
    + +
    + +
    + +
    +
    +
    +
    +
    +} \ No newline at end of file diff --git a/src/main/twirl/admin/system.scala.html b/src/main/twirl/admin/system.scala.html index 6594e29..13a247d 100644 --- a/src/main/twirl/admin/system.scala.html +++ b/src/main/twirl/admin/system.scala.html @@ -1,4 +1,4 @@ -@(settings: service.SystemSettingsService.SystemSettings, info: Option[Any])(implicit context: app.Context) +@(info: Option[Any])(implicit context: app.Context) @import context._ @import util.Directory._ @import view.helpers._ @@ -22,9 +22,10 @@
    +
    -

    +

    The base URL is used for redirect, notification email, git repository URL box and more. If the base URL is empty, GitBucket generates URL from request information. You can use this property to adjust URL difference between the reverse proxy and GitBucket. @@ -56,6 +57,29 @@ + + +


    + +
    + +
    +
    +
    + +
    + + +
    +
    +
    +

    + Base URL is required if SSH access is enabled. +

    +
    @@ -206,6 +230,10 @@ } \ No newline at end of file diff --git a/src/main/twirl/header.scala.html b/src/main/twirl/header.scala.html index 55f19ee..39b1566 100644 --- a/src/main/twirl/header.scala.html +++ b/src/main/twirl/header.scala.html @@ -6,7 +6,7 @@ } @@ -56,7 +56,7 @@ Network - @if(loginAccount.isDefined && (loginAccount.get.isAdmin || loginAccount.get.userName == repository.owner)){ + @if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){ Settings diff --git a/src/main/twirl/helper/copy.scala.html b/src/main/twirl/helper/copy.scala.html index 1cba6e6..a4f8896 100644 --- a/src/main/twirl/helper/copy.scala.html +++ b/src/main/twirl/helper/copy.scala.html @@ -1,5 +1,5 @@ -@(id: String, value: String)(html: Html) -
    +@(id: String, value: String, prepend: Boolean = false)(html: Html) +
    @html
    diff --git a/src/main/twirl/index.scala.html b/src/main/twirl/index.scala.html index c6e3de6..21db2e0 100644 --- a/src/main/twirl/index.scala.html +++ b/src/main/twirl/index.scala.html @@ -1,6 +1,5 @@ @(activities: List[model.Activity], recentRepositories: List[service.RepositoryService.RepositoryInfo], - systemSettings: service.SystemSettingsService.SystemSettings, userRepositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context) @import context._ @import view.helpers._ @@ -12,7 +11,7 @@
    @if(loginAccount.isEmpty){ - @signinform(systemSettings) + @signinform(settings) } else { diff --git a/src/main/twirl/issues/create.scala.html b/src/main/twirl/issues/create.scala.html index 7b92e2c..b82884c 100644 --- a/src/main/twirl/issues/create.scala.html +++ b/src/main/twirl/issues/create.scala.html @@ -33,7 +33,7 @@ @helper.html.dropdown() {
  • No milestone
  • - @milestones.map { milestone => + @milestones.filter(_.closedDate.isEmpty).map { milestone =>
  • @milestone.title diff --git a/src/main/twirl/issues/issuedetail.scala.html b/src/main/twirl/issues/issuedetail.scala.html index 7a61819..df678d1 100644 --- a/src/main/twirl/issues/issuedetail.scala.html +++ b/src/main/twirl/issues/issuedetail.scala.html @@ -54,7 +54,7 @@ @if(hasWritePermission){ @helper.html.dropdown() {
  • No milestone
  • - @milestones.map { case (milestone, _, _) => + @milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
  • @helper.html.checkicon(Some(milestone.milestoneId) == issue.milestoneId) @milestone.title diff --git a/src/main/twirl/main.scala.html b/src/main/twirl/main.scala.html index 7ec9842..51ba07b 100644 --- a/src/main/twirl/main.scala.html +++ b/src/main/twirl/main.scala.html @@ -54,14 +54,18 @@ } @if(loginAccount.isDefined){ @avatar(loginAccount.get.userName, 20) @loginAccount.get.userName - + + @if(loginAccount.get.isAdmin){ } } else { - Sign in + Sign in } @@ -76,7 +80,6 @@ $('#search').submit(function(){ return $.trim($(this).find('input[name=query]').val()) != ''; }); - $('#signin').attr('href', '@path/signin?redirect=' + encodeURIComponent(location.pathname + location.search + location.hash)); }); diff --git a/src/main/twirl/newrepo.scala.html b/src/main/twirl/newrepo.scala.html deleted file mode 100644 index 05fefbc..0000000 --- a/src/main/twirl/newrepo.scala.html +++ /dev/null @@ -1,73 +0,0 @@ -@(groupNames: List[String])(implicit context: app.Context) -@import context._ -@import view.helpers._ -@main("Create a New Repository"){ -
    -
    -
    - -
    - - - -
    - / - - -
    -
    - - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    - -
    -} - diff --git a/src/main/twirl/repo/branches.scala.html b/src/main/twirl/repo/branches.scala.html index 219c7c1..bc26459 100644 --- a/src/main/twirl/repo/branches.scala.html +++ b/src/main/twirl/repo/branches.scala.html @@ -1,4 +1,4 @@ -@(branchInfo: List[(String, java.util.Date)], +@(branchInfo: Seq[(String, java.util.Date)], hasWritePermission: Boolean, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) @import context._ diff --git a/src/main/twirl/repo/files.scala.html b/src/main/twirl/repo/files.scala.html index c7e97e5..db21928 100644 --- a/src/main/twirl/repo/files.scala.html +++ b/src/main/twirl/repo/files.scala.html @@ -55,14 +55,22 @@
  • @if(file.isDirectory){ - + @if(file.linkUrl.isDefined){ + + } else { + + } } else { } @if(file.isDirectory){ - @file.name + @if(file.linkUrl.isDefined){ + @file.name + } else { + @file.name + } } else { @file.name } diff --git a/src/main/twirl/repo/guide.scala.html b/src/main/twirl/repo/guide.scala.html index ad02313..a5c7738 100644 --- a/src/main/twirl/repo/guide.scala.html +++ b/src/main/twirl/repo/guide.scala.html @@ -9,13 +9,13 @@ git init git add README.md git commit -m "first commit" -git remote add origin @repository.url +git remote add origin @repository.httpUrl git push -u origin master

    Push an existing repository from the command line

    -git remote add origin @repository.url
    +git remote add origin @repository.httpUrl
     git push -u origin master
     
    } diff --git a/src/main/twirl/repo/tab.scala.html b/src/main/twirl/repo/tab.scala.html index c245780..0d9c28c 100644 --- a/src/main/twirl/repo/tab.scala.html +++ b/src/main/twirl/repo/tab.scala.html @@ -22,8 +22,15 @@ Branches@if(repository.branchList.length > 0){ @repository.branchList.length} Tags@if(repository.tags.length > 0){ @repository.tags.length}
  • - @helper.html.copy("repository-url-copy", repository.url){ - + @helper.html.copy("repository-url-copy", repository.httpUrl, true){ + @if(settings.ssh && loginAccount.isDefined){ +
    + +
    + } else { + HTTP + } + }
  • @@ -32,3 +39,17 @@
  • +@if(settings.ssh && loginAccount.isDefined){ + +} \ No newline at end of file diff --git a/src/main/twirl/settings/collaborators.scala.html b/src/main/twirl/settings/collaborators.scala.html index 8264764..cf894a4 100644 --- a/src/main/twirl/settings/collaborators.scala.html +++ b/src/main/twirl/settings/collaborators.scala.html @@ -13,6 +13,10 @@ @collaboratorName @if(!isGroupRepository){ (remove) + } else { + @if(repository.managers.contains(collaboratorName)){ + (Manager) + } } } diff --git a/src/main/twirl/signin.scala.html b/src/main/twirl/signin.scala.html index abe033e..f70e844 100644 --- a/src/main/twirl/signin.scala.html +++ b/src/main/twirl/signin.scala.html @@ -1,7 +1,7 @@ -@(systemSettings: service.SystemSettingsService.SystemSettings)(implicit context: app.Context) +@()(implicit context: app.Context) @import context._ @main("Sign in"){ } diff --git a/src/main/twirl/wiki/pages.scala.html b/src/main/twirl/wiki/pages.scala.html index d1d35f2..429c438 100644 --- a/src/main/twirl/wiki/pages.scala.html +++ b/src/main/twirl/wiki/pages.scala.html @@ -1,4 +1,6 @@ -@(pages: List[String], repository: service.RepositoryService.RepositoryInfo, hasWritePermission: Boolean)(implicit context: app.Context) +@(pages: List[String], + repository: service.RepositoryService.RepositoryInfo, + hasWritePermission: Boolean)(implicit context: app.Context) @import context._ @import view.helpers._ @html.main(s"Pages - ${repository.owner}/${repository.name}", Some(repository)){ diff --git a/src/main/twirl/wiki/tab.scala.html b/src/main/twirl/wiki/tab.scala.html index 959ea8c..e68c885 100644 --- a/src/main/twirl/wiki/tab.scala.html +++ b/src/main/twirl/wiki/tab.scala.html @@ -1,15 +1,36 @@ -@(active: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) +@(active: String, + repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) @import context._ +@import service.WikiService._ @import view.helpers._ +@if(settings.ssh && loginAccount.isDefined){ + +} diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index d57d64e..398ffd8 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -18,6 +18,7 @@ servlet.AutoUpdateListener + @@ -26,6 +27,13 @@ + + + + ssh.SshServerListener + + + diff --git a/src/main/webapp/assets/common/css/gitbucket.css b/src/main/webapp/assets/common/css/gitbucket.css index 01c96b4..c8cd501 100644 --- a/src/main/webapp/assets/common/css/gitbucket.css +++ b/src/main/webapp/assets/common/css/gitbucket.css @@ -80,19 +80,13 @@ text-decoration: none; } -div.input-prepend span.add-on { +div.input-prepend span.count { background-color: white; -webkit-border-radius: 0 4px 4px 0; -moz-border-radius: 0 4px 4px 0; border-radius: 0 4px 4px 0; } -/* -div.input-prepend span.add-on a { - color: #333; -} -*/ - /* ======================================================================== */ /* General Styles */ /* ======================================================================== */ @@ -464,10 +458,9 @@ li.L0, li.L1, li.L2, li.L3, li.L4, li.L5, li.L6, li.L7, li.L8, li.L9 { list-style-type: decimal; background: white; -} - -li.L1, li.L3, li.L5, li.L7, li.L9 { - background: #f5f5f5; + border-left: 1px solid #E5E5E5; + padding-left: 10px; + color: rgba(0, 0, 0, 0.3); } pre.blob { diff --git a/src/main/webapp/assets/common/images/folder_link.png b/src/main/webapp/assets/common/images/folder_link.png new file mode 100644 index 0000000..b9b75f6 --- /dev/null +++ b/src/main/webapp/assets/common/images/folder_link.png Binary files differ diff --git a/src/main/webapp/assets/common/js/gitbucket.js b/src/main/webapp/assets/common/js/gitbucket.js index 12e86fa..26df149 100644 --- a/src/main/webapp/assets/common/js/gitbucket.js +++ b/src/main/webapp/assets/common/js/gitbucket.js @@ -34,3 +34,14 @@ // syntax highlighting by google-code-prettify prettyPrint(); }); + +function displayErrors(data){ + var i = 0; + $.each(data, function(key, value){ + $('#error-' + key.split(".").join("_")).text(value); + if(i == 0){ + $('#' + key).focus(); + } + i++; + }); +} \ No newline at end of file diff --git a/src/main/webapp/assets/jsdifflib/diffview.css b/src/main/webapp/assets/jsdifflib/diffview.css index 4c4a7c7..399a6c7 100644 --- a/src/main/webapp/assets/jsdifflib/diffview.css +++ b/src/main/webapp/assets/jsdifflib/diffview.css @@ -35,12 +35,22 @@ table.diff tbody { font-family:Courier, monospace } +table.diff tbody tr:hover { + background-color:#F8EEC7; +} + +table.diff tbody tr:hover th { + background-color:#F6E8B5; +} +table.diff tbody tr:hover td { + background-color:#F6E8B5; +} table.diff tbody th { font-family:verdana,arial,'Bitstream Vera Sans',helvetica,sans-serif; - background:#EED; + background-color:#FBFBFB; font-size:11px; font-weight:normal; - border:1px solid #BBC; + border-top:none; /* for overriding bootstrap */ color:#886; padding:.3em .5em .1em 2em; text-align:right; @@ -58,6 +68,7 @@ padding:0px .4em; padding-top:.4em; vertical-align:top; + border-top: none; } table.diff .empty { background-color:#DDD; @@ -69,9 +80,10 @@ background-color:#FFDDDD; } table.diff .skip { - background-color:#EFEFEF; - border:1px solid #AAA; - border-right:1px solid #BBC; + background-color: #F8F8FF; +} +table.diff .skip:before { + content: " ..."; } table.diff .insert { background-color:#DDFFDD diff --git a/src/test/scala/service/AccountServiceServiceSpec.scala b/src/test/scala/service/AccountServiceServiceSpec.scala deleted file mode 100644 index ba53327..0000000 --- a/src/test/scala/service/AccountServiceServiceSpec.scala +++ /dev/null @@ -1,78 +0,0 @@ -package service - -import org.specs2.mutable.Specification -import java.util.Date - -class AccountServiceServiceSpec extends Specification with ServiceSpecBase { - - "AccountService" should { - val RootMailAddress = "root@localhost" - - "getAllUsers" in { withTestDB{ - AccountService.getAllUsers() must be like{ - case List(model.Account("root", "root", RootMailAddress, _, true, _, _, _, None, None, false, false)) => ok - } - }} - - "getAccountByUserName" in { withTestDB{ - AccountService.getAccountByUserName("root") must beSome.like{ - case user => user.userName must_== "root" - } - - AccountService.getAccountByUserName("invalid user name") must beNone - }} - - "getAccountByMailAddress" in { withTestDB{ - AccountService.getAccountByMailAddress(RootMailAddress) must beSome - }} - - "updateLastLoginDate" in { withTestDB{ - val root = "root" - def user() = - AccountService.getAccountByUserName(root).getOrElse(sys.error(s"user $root does not exists")) - - user().lastLoginDate must beNone - val date1 = new Date - AccountService.updateLastLoginDate(root) - user().lastLoginDate must beSome.like{ case date => - date must be_>(date1) - } - val date2 = new Date - Thread.sleep(1000) - AccountService.updateLastLoginDate(root) - user().lastLoginDate must beSome.like{ case date => - date must be_>(date2) - } - }} - - "updateAccount" in { withTestDB{ - val root = "root" - def user() = - AccountService.getAccountByUserName(root).getOrElse(sys.error(s"user $root does not exists")) - - val newAddress = "new mail address" - AccountService.updateAccount(user().copy(mailAddress = newAddress)) - user().mailAddress must_== newAddress - }} - - "group" in { withTestDB { - val group1 = "group1" - val user1 = "root" - AccountService.createGroup(group1, None) - - AccountService.getGroupMembers(group1) must_== Nil - AccountService.getGroupsByUserName(user1) must_== Nil - - AccountService.updateGroupMembers(group1, List(user1)) - - AccountService.getGroupMembers(group1) must_== List(user1) - AccountService.getGroupsByUserName(user1) must_== List(group1) - - AccountService.updateGroupMembers(group1, Nil) - - AccountService.getGroupMembers(group1) must_== Nil - AccountService.getGroupsByUserName(user1) must_== Nil - }} - } -} - diff --git a/src/test/scala/service/AccountServiceSpec.scala b/src/test/scala/service/AccountServiceSpec.scala new file mode 100644 index 0000000..344f8e3 --- /dev/null +++ b/src/test/scala/service/AccountServiceSpec.scala @@ -0,0 +1,79 @@ +package service + +import org.specs2.mutable.Specification +import java.util.Date +import model.GroupMember + +class AccountServiceSpec extends Specification with ServiceSpecBase { + + "AccountService" should { + val RootMailAddress = "root@localhost" + + "getAllUsers" in { withTestDB{ + AccountService.getAllUsers() must be like{ + case List(model.Account("root", "root", RootMailAddress, _, true, _, _, _, None, None, false, false)) => ok + } + }} + + "getAccountByUserName" in { withTestDB{ + AccountService.getAccountByUserName("root") must beSome.like{ + case user => user.userName must_== "root" + } + + AccountService.getAccountByUserName("invalid user name") must beNone + }} + + "getAccountByMailAddress" in { withTestDB{ + AccountService.getAccountByMailAddress(RootMailAddress) must beSome + }} + + "updateLastLoginDate" in { withTestDB{ + val root = "root" + def user() = + AccountService.getAccountByUserName(root).getOrElse(sys.error(s"user $root does not exists")) + + user().lastLoginDate must beNone + val date1 = new Date + AccountService.updateLastLoginDate(root) + user().lastLoginDate must beSome.like{ case date => + date must be_>(date1) + } + val date2 = new Date + Thread.sleep(1000) + AccountService.updateLastLoginDate(root) + user().lastLoginDate must beSome.like{ case date => + date must be_>(date2) + } + }} + + "updateAccount" in { withTestDB{ + val root = "root" + def user() = + AccountService.getAccountByUserName(root).getOrElse(sys.error(s"user $root does not exists")) + + val newAddress = "new mail address" + AccountService.updateAccount(user().copy(mailAddress = newAddress)) + user().mailAddress must_== newAddress + }} + + "group" in { withTestDB { + val group1 = "group1" + val user1 = "root" + AccountService.createGroup(group1, None) + + AccountService.getGroupMembers(group1) must_== Nil + AccountService.getGroupsByUserName(user1) must_== Nil + + AccountService.updateGroupMembers(group1, List((user1, true))) + + AccountService.getGroupMembers(group1) must_== List(GroupMember(group1, user1, true)) + AccountService.getGroupsByUserName(user1) must_== List(group1) + + AccountService.updateGroupMembers(group1, Nil) + + AccountService.getGroupMembers(group1) must_== Nil + AccountService.getGroupsByUserName(user1) must_== Nil + }} + } +} + diff --git a/src/test/scala/ssh/GitCommandSpec.scala b/src/test/scala/ssh/GitCommandSpec.scala new file mode 100644 index 0000000..c5c4630 --- /dev/null +++ b/src/test/scala/ssh/GitCommandSpec.scala @@ -0,0 +1,40 @@ +package ssh + +import org.specs2.mutable._ +import org.specs2.mock.Mockito +import org.apache.sshd.server.command.UnknownCommand +import javax.servlet.ServletContext + +class GitCommandFactorySpec extends Specification with Mockito { + + val factory = new GitCommandFactory(mock[ServletContext], "http://localhost:8080") + + "createCommand" should { + "returns GitReceivePack when command is git-receive-pack" in { + factory.createCommand("git-receive-pack '/owner/repo.git'").isInstanceOf[GitReceivePack] must beTrue + factory.createCommand("git-receive-pack '/owner/repo.wiki.git'").isInstanceOf[GitReceivePack] must beTrue + + } + "returns GitUploadPack when command is git-upload-pack" in { + factory.createCommand("git-upload-pack '/owner/repo.git'").isInstanceOf[GitUploadPack] must beTrue + factory.createCommand("git-upload-pack '/owner/repo.wiki.git'").isInstanceOf[GitUploadPack] must beTrue + + } + "returns UnknownCommand when command is not git-(upload|receive)-pack" in { + factory.createCommand("git- '/owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue + factory.createCommand("git-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue + factory.createCommand("git-a-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue + factory.createCommand("git-up-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue + factory.createCommand("\ngit-upload-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue + } + "returns UnknownCommand when git command has no valid arguments" in { + // must be: git-upload-pack '/owner/repository_name.git' + factory.createCommand("git-upload-pack").isInstanceOf[UnknownCommand] must beTrue + factory.createCommand("git-upload-pack /owner/repo.git").isInstanceOf[UnknownCommand] must beTrue + factory.createCommand("git-upload-pack 'owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue + factory.createCommand("git-upload-pack '/ownerrepo.git'").isInstanceOf[UnknownCommand] must beTrue + factory.createCommand("git-upload-pack '/owner/repo.wiki'").isInstanceOf[UnknownCommand] must beTrue + } + } + +} diff --git a/src/test/scala/view/AvatarImageProviderSpec.scala b/src/test/scala/view/AvatarImageProviderSpec.scala index f337316..4a00c4f 100644 --- a/src/test/scala/view/AvatarImageProviderSpec.scala +++ b/src/test/scala/view/AvatarImageProviderSpec.scala @@ -10,53 +10,58 @@ class AvatarImageProviderSpec extends Specification { - implicit val context = app.Context("", None, null) - "getAvatarImageHtml" should { "show Gravatar image for no image account if gravatar integration is enabled" in { - val provider = new AvatarImageProviderImpl(Some(createAccount(None)), createSystemSettings(true)) + implicit val context = app.Context(createSystemSettings(true), None, null) + val provider = new AvatarImageProviderImpl(Some(createAccount(None))) provider.toHtml("user", 20).toString mustEqual "" } "show uploaded image even if gravatar integration is enabled" in { - val provider = new AvatarImageProviderImpl(Some(createAccount(Some("icon.png"))), createSystemSettings(true)) + implicit val context = app.Context(createSystemSettings(true), None, null) + val provider = new AvatarImageProviderImpl(Some(createAccount(Some("icon.png")))) provider.toHtml("user", 20).toString mustEqual "" } "show local image for no image account if gravatar integration is disabled" in { - val provider = new AvatarImageProviderImpl(Some(createAccount(None)), createSystemSettings(false)) + implicit val context = app.Context(createSystemSettings(false), None, null) + val provider = new AvatarImageProviderImpl(Some(createAccount(None))) provider.toHtml("user", 20).toString mustEqual "" } "show Gravatar image for specified mail address if gravatar integration is enabled" in { - val provider = new AvatarImageProviderImpl(None, createSystemSettings(true)) + implicit val context = app.Context(createSystemSettings(true), None, null) + val provider = new AvatarImageProviderImpl(None) provider.toHtml("user", 20, "hoge@hoge.com").toString mustEqual "" } "show unknown image for unknown user if gravatar integration is enabled" in { - val provider = new AvatarImageProviderImpl(None, createSystemSettings(true)) + implicit val context = app.Context(createSystemSettings(true), None, null) + val provider = new AvatarImageProviderImpl(None) provider.toHtml("user", 20).toString mustEqual "" } "show unknown image for specified mail address if gravatar integration is disabled" in { - val provider = new AvatarImageProviderImpl(None, createSystemSettings(false)) + implicit val context = app.Context(createSystemSettings(false), None, null) + val provider = new AvatarImageProviderImpl(None) provider.toHtml("user", 20, "hoge@hoge.com").toString mustEqual "" } "add tooltip if it's enabled" in { - val provider = new AvatarImageProviderImpl(None, createSystemSettings(false)) + implicit val context = app.Context(createSystemSettings(false), None, null) + val provider = new AvatarImageProviderImpl(None) provider.toHtml("user", 20, "hoge@hoge.com", true).toString mustEqual "" @@ -80,10 +85,12 @@ private def createSystemSettings(useGravatar: Boolean) = SystemSettings( - baseUrl = None, + baseUrl = Some(""), allowAccountRegistration = false, gravatar = useGravatar, notification = false, + ssh = false, + sshPort = None, smtp = None, ldapAuthentication = false, ldap = None) @@ -91,15 +98,13 @@ /** * Adapter to test AvatarImageProviderImpl. */ - class AvatarImageProviderImpl(account: Option[Account], settings: SystemSettings) - extends AvatarImageProvider with RequestCache { + class AvatarImageProviderImpl(account: Option[Account]) extends AvatarImageProvider with RequestCache { def toHtml(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false) (implicit context: app.Context): Html = getAvatarImageHtml(userName, size, mailAddress, tooltip) override def getAccountByMailAddress(mailAddress: String)(implicit context: app.Context): Option[Account] = account override def getAccountByUserName(userName: String)(implicit context: app.Context): Option[Account] = account - override def getSystemSettings()(implicit context: app.Context): SystemSettings = settings } }