diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/README.md b/README.md
index 95a0822..e56432c 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
GitBucket
=========
-GitBucket is a Github clone by Scala, Easy to setup.
+GitBucket is the easily installable Github clone written with Scala.
The current version of GitBucket provides a basic features below:
@@ -9,12 +9,12 @@
- Repository viewer (some advanced features are not implemented)
- Wiki
- Issues
+- Activity timeline
- User management (for Administrators)
Following features are not implemented, but we will make them in the future release!
- Fork and pull request
-- Timeline
- Search
- Network graph
- Statics
@@ -32,8 +32,20 @@
The default administrator account is **root** and password is **root**.
+To upgrade GitBucket, only replace gitbucket.war.
+
Release Notes
--------
+### 1.3 - 18 Jul 2013
+- Batch updating for issues.
+- Display assigned user on issue list.
+- User icon and Gravatar support.
+- Convert @xxxx to link to the account page.
+- Add copy to clipboard button for git clone URL.
+- Allows multi-byte characters as wiki page name.
+- Allows to create the empty repository.
+- Fixed some bugs.
+
### 1.2 - 09 Jul 2013
- Added activity timeline.
- Bugfix for Git 1.8.1.5 or later.
diff --git a/src/main/resources/update/1_3.sql b/src/main/resources/update/1_3.sql
index 4227448..59ab009 100644
--- a/src/main/resources/update/1_3.sql
+++ b/src/main/resources/update/1_3.sql
@@ -1,4 +1,8 @@
ALTER TABLE ACCOUNT ADD COLUMN IMAGE VARCHAR(100);
-ALTER TABLE REPOSITORY ADD COLUMN ORIGIN_USER_NAME VARCHAR(100);
-ALTER TABLE REPOSITORY ADD COLUMN ORIGIN_REPOSITORY_NAME VARCHAR(100);
+UPDATE ISSUE_COMMENT SET ACTION = 'comment' WHERE ACTION IS NULL;
+
+ALTER TABLE ISSUE_COMMENT ALTER COLUMN ACTION VARCHAR(20) NOT NULL;
+
+UPDATE ISSUE_COMMENT SET ACTION = 'close_comment' WHERE ACTION = 'close';
+UPDATE ISSUE_COMMENT SET ACTION = 'reopen_comment' WHERE ACTION = 'reopen';
diff --git a/src/main/resources/update/1_4.sql b/src/main/resources/update/1_4.sql
index 4750019..a68ca43 100644
--- a/src/main/resources/update/1_4.sql
+++ b/src/main/resources/update/1_4.sql
@@ -1,15 +1,10 @@
-CREATE TABLE PULL_REQUEST(
- USER_NAME VARCHAR(100) NOT NULL,
- REPOSITORY_NAME VARCHAR(100) NOT NULL,
- ISSUE_ID INT NOT NULL,
- BRANCH VARCHAR(100) NOT NULL,
- REQUEST_USER_NAME VARCHAR(100) NOT NULL,
- REQUEST_REPOSITORY_NAME VARCHAR(100) NOT NULL,
- REQUEST_BRANCH VARCHAR(100) NOT NULL,
- MERGE_START_ID VARCHAR(40),
- MERGE_END_ID VARCHAR(40)
+CREATE TABLE GROUP_MEMBER(
+ GROUP_NAME VARCHAR(100) NOT NULL,
+ USER_NAME VARCHAR(100) NOT NULL
);
-ALTER TABLE PULL_REQUEST ADD CONSTRAINT IDX_PULL_REQUEST_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
-ALTER TABLE PULL_REQUEST ADD CONSTRAINT IDX_PULL_REQUEST_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
-ALTER TABLE PULL_REQUEST ADD CONSTRAINT IDX_PULL_REQUEST_FK1 FOREIGN KEY (REQUEST_USER_NAME, REQUEST_REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
+ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_PK PRIMARY KEY (GROUP_NAME, USER_NAME);
+ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_FK0 FOREIGN KEY (GROUP_NAME) REFERENCES ACCOUNT (USER_NAME);
+ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_FK1 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);
+
+ALTER TABLE ACCOUNT ADD COLUMN GROUP_ACCOUNT BOOLEAN NOT NULL DEFAULT FALSE;
diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala
index 69ba545..60a8dbb 100644
--- a/src/main/scala/ScalatraBootstrap.scala
+++ b/src/main/scala/ScalatraBootstrap.scala
@@ -5,6 +5,7 @@
class ScalatraBootstrap extends LifeCycle {
override def init(context: ServletContext) {
context.mount(new IndexController, "/")
+ context.mount(new SearchController, "/")
context.mount(new FileUploadController, "/upload")
context.mount(new SignInController, "/*")
context.mount(new UserManagementController, "/*")
diff --git a/src/main/scala/app/AccountController.scala b/src/main/scala/app/AccountController.scala
index 68b2d53..acf066f 100644
--- a/src/main/scala/app/AccountController.scala
+++ b/src/main/scala/app/AccountController.scala
@@ -1,11 +1,10 @@
package app
import service._
-import util.{FileUtil, FileUploadUtil, OneselfAuthenticator}
+import util.{FileUtil, OneselfAuthenticator}
import util.StringUtil._
import util.Directory._
import jp.sf.amateras.scalatra.forms._
-import org.apache.commons.io.FileUtils
import org.scalatra.FlashMapSupport
class AccountController extends AccountControllerBase
@@ -43,12 +42,23 @@
*/
get("/:userName") {
val userName = params("userName")
- getAccountByUserName(userName).map { x =>
+ getAccountByUserName(userName).map { account =>
params.getOrElse("tab", "repositories") match {
// Public Activity
- case "activity" => account.html.activity(x, getActivitiesByUser(userName, true))
+ case "activity" =>
+ _root_.account.html.activity(account,
+ if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
+ getActivitiesByUser(userName, true))
+
+ // Members
+ case "members" if(account.isGroupAccount) =>
+ _root_.account.html.members(account, getGroupMembers(account.userName))
+
// Repositories
- case _ => account.html.repositories(x, getVisibleRepositories(userName, baseUrl, context.loginAccount.map(_.userName)))
+ case _ =>
+ _root_.account.html.repositories(account,
+ if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
+ getVisibleRepositories(userName, baseUrl, context.loginAccount.map(_.userName)))
}
} getOrElse NotFound
}
diff --git a/src/main/scala/app/ControllerBase.scala b/src/main/scala/app/ControllerBase.scala
index b338f36..bff5994 100644
--- a/src/main/scala/app/ControllerBase.scala
+++ b/src/main/scala/app/ControllerBase.scala
@@ -1,7 +1,7 @@
package app
import _root_.util.Directory._
-import _root_.util.{FileUploadUtil, FileUtil, Validations}
+import _root_.util.{FileUtil, Validations}
import org.scalatra._
import org.scalatra.json._
import org.json4s._
@@ -10,7 +10,9 @@
import model.Account
import scala.Some
import service.AccountService
-import javax.servlet.http.HttpServletRequest
+import javax.servlet.http.{HttpSession, HttpServletRequest}
+import java.text.SimpleDateFormat
+import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
/**
* Provides generic features for controller implementations.
@@ -20,6 +22,21 @@
implicit val jsonFormats = DefaultFormats
+ override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
+ val httpRequest = request.asInstanceOf[HttpServletRequest]
+ val path = httpRequest.getRequestURI.substring(request.getServletContext.getContextPath.length)
+
+ if(path.startsWith("/console/")){
+ Option(httpRequest.getSession.getAttribute("LOGIN_ACCOUNT").asInstanceOf[Account]).collect {
+ case account if(account.isAdmin) => chain.doFilter(request, response)
+ }
+ } else if(path.startsWith("/git/")){
+ chain.doFilter(request, response)
+ } else {
+ super.doFilter(request, response, chain)
+ }
+ }
+
/**
* Returns the context object for the request.
*/
@@ -116,7 +133,8 @@
/**
* Base trait for controllers which manages account information.
*/
-trait AccountManagementControllerBase extends ControllerBase { self: AccountService =>
+trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase {
+ self: AccountService =>
protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit = {
if(clearImage){
@@ -126,9 +144,9 @@
}
} else {
fileId.map { fileId =>
- val filename = "avatar." + FileUtil.getExtension(FileUploadUtil.getUploadedFilename(fileId).get)
+ val filename = "avatar." + FileUtil.getExtension(getUploadedFilename(fileId).get)
FileUtils.moveFile(
- FileUploadUtil.getTemporaryFile(fileId),
+ getTemporaryFile(fileId),
new java.io.File(getUserUploadDir(userName), filename)
)
updateAvatarImage(userName, Some(filename))
@@ -148,4 +166,34 @@
.map { _ => "Mail address is already registered." }
}
+}
+
+/**
+ * Base trait for controllers which needs file uploading feature.
+ */
+trait FileUploadControllerBase {
+
+ def generateFileId: String =
+ new SimpleDateFormat("yyyyMMddHHmmSSsss").format(new java.util.Date(System.currentTimeMillis))
+
+ def TemporaryDir(implicit session: HttpSession): java.io.File =
+ new java.io.File(GitBucketHome, s"tmp/_upload/${session.getId}")
+
+ def getTemporaryFile(fileId: String)(implicit session: HttpSession): java.io.File =
+ new java.io.File(TemporaryDir, fileId)
+
+ // def removeTemporaryFile(fileId: String)(implicit session: HttpSession): Unit =
+ // getTemporaryFile(fileId).delete()
+
+ def removeTemporaryFiles()(implicit session: HttpSession): Unit =
+ FileUtils.deleteDirectory(TemporaryDir)
+
+ def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] = {
+ val filename = Option(session.getAttribute("upload_" + fileId).asInstanceOf[String])
+ if(filename.isDefined){
+ session.removeAttribute("upload_" + fileId)
+ }
+ filename
+ }
+
}
\ No newline at end of file
diff --git a/src/main/scala/app/CreateRepositoryController.scala b/src/main/scala/app/CreateRepositoryController.scala
index 30e2d22..0aa12e9 100644
--- a/src/main/scala/app/CreateRepositoryController.scala
+++ b/src/main/scala/app/CreateRepositoryController.scala
@@ -5,9 +5,9 @@
import service._
import java.io.File
import org.eclipse.jgit.api.Git
-import org.eclipse.jgit.lib._
import org.apache.commons.io._
import jp.sf.amateras.scalatra.forms._
+import org.eclipse.jgit.lib.PersonIdent
class CreateRepositoryController extends CreateRepositoryControllerBase
with RepositoryService with AccountService with WikiService with LabelsService with ActivityService
@@ -17,14 +17,15 @@
* Creates new repository.
*/
trait CreateRepositoryControllerBase extends ControllerBase {
- self: RepositoryService with WikiService with LabelsService with ActivityService
+ self: RepositoryService with AccountService with WikiService with LabelsService with ActivityService
with UsersAuthenticator with ReferrerAuthenticator =>
- case class RepositoryCreationForm(name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
+ 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())),
@@ -40,28 +41,36 @@
* Show the new repository form.
*/
get("/new")(usersOnly {
- html.newrepo()
+ html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
})
/**
* Create new repository.
*/
post("/new", newForm)(usersOnly { form =>
+ val ownerAccount = getAccountByUserName(form.owner).get
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
// Insert to the database at first
- createRepository(form.name, loginUserName, form.description, form.isPrivate)
+ 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(loginUserName, form.name)
// Create the actual repository
- val gitdir = getRepositoryDir(loginUserName, form.name)
+ val gitdir = getRepositoryDir(form.owner, form.name)
JGitUtil.initRepository(gitdir)
if(form.createReadme){
- val tmpdir = getInitRepositoryDir(loginUserName, form.name)
+ val tmpdir = getInitRepositoryDir(form.owner, form.name)
try {
// Clone the repository
Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call
@@ -91,13 +100,13 @@
}
// Create Wiki repository
- createWikiRepository(loginAccount, form.name)
+ createWikiRepository(loginAccount, form.owner, form.name)
// Record activity
- recordCreateRepositoryActivity(loginUserName, form.name, loginUserName)
+ recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
// redirect to the repository
- redirect(s"/${loginUserName}/${form.name}")
+ redirect(s"/${form.owner}/${form.name}")
})
post("/:owner/:repository/_fork")(referrersOnly { repository =>
@@ -153,12 +162,19 @@
createLabel(userName, repositoryName, "wontfix", "ffffff")
}
+ private def existsAccount: Constraint = new Constraint(){
+ def validate(name: String, value: String): 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(){
def validate(name: String, value: String): Option[String] =
- getRepositoryNamesOfUser(context.loginAccount.get.userName).find(_ == value).map(_ => "Repository already exists.")
+ params.get("owner").flatMap { userName =>
+ getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
+ }
}
}
\ No newline at end of file
diff --git a/src/main/scala/app/FileUploadController.scala b/src/main/scala/app/FileUploadController.scala
index 5d5ed3b..350dfed 100644
--- a/src/main/scala/app/FileUploadController.scala
+++ b/src/main/scala/app/FileUploadController.scala
@@ -1,6 +1,6 @@
package app
-import util.{FileUtil, FileUploadUtil}
+import util.{FileUtil}
import org.scalatra._
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport}
import org.apache.commons.io.FileUtils
@@ -9,17 +9,18 @@
* Provides Ajax based file upload functionality.
*
* This servlet saves uploaded file as temporary file and returns the unique id.
- * You can get uploaded file using [[util.FileUploadUtil#getTemporaryFile()]] with this id.
+ * You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id.
*/
-// TODO Remove temporary files at session timeout by session listener.
-class FileUploadController extends ScalatraServlet with FileUploadSupport with FlashMapSupport {
+class FileUploadController extends ScalatraServlet
+ with FileUploadSupport with FlashMapSupport with FileUploadControllerBase {
+
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
post("/image"){
fileParams.get("file") match {
case Some(file) if(FileUtil.isImage(file.name)) => {
- val fileId = FileUploadUtil.generateFileId
- FileUtils.writeByteArrayToFile(FileUploadUtil.getTemporaryFile(fileId), file.get)
+ val fileId = generateFileId
+ FileUtils.writeByteArrayToFile(getTemporaryFile(fileId), file.get)
session += "upload_" + fileId -> file.name
Ok(fileId)
}
diff --git a/src/main/scala/app/IndexController.scala b/src/main/scala/app/IndexController.scala
index c16873b..5612f9b 100644
--- a/src/main/scala/app/IndexController.scala
+++ b/src/main/scala/app/IndexController.scala
@@ -1,13 +1,17 @@
package app
+import util._
import service._
+import jp.sf.amateras.scalatra.forms._
class IndexController extends IndexControllerBase
- with RepositoryService with AccountService with SystemSettingsService with ActivityService
+ with RepositoryService with SystemSettingsService with ActivityService with AccountService
+with UsersAuthenticator
-trait IndexControllerBase extends ControllerBase { self: RepositoryService
- with SystemSettingsService with ActivityService =>
-
+trait IndexControllerBase extends ControllerBase {
+ self: RepositoryService with SystemSettingsService with ActivityService with AccountService
+ with UsersAuthenticator =>
+
get("/"){
val loginAccount = context.loginAccount
@@ -18,4 +22,14 @@
)
}
-}
\ No newline at end of file
+ /**
+ * JSON API for collaborator completion.
+ */
+ // TODO Move to other controller?
+ get("/_user/proposals")(usersOnly {
+ contentType = formats("json")
+ org.json4s.jackson.Serialization.write(Map("options" -> getAllUsers.filter(!_.isGroupAccount).map(_.userName).toArray))
+ })
+
+
+}
diff --git a/src/main/scala/app/IssuesController.scala b/src/main/scala/app/IssuesController.scala
index 307610d..0c8b35f 100644
--- a/src/main/scala/app/IssuesController.scala
+++ b/src/main/scala/app/IssuesController.scala
@@ -67,7 +67,7 @@
getComments(owner, name, issueId.toInt),
getIssueLabels(owner, name, issueId.toInt),
(getCollaborators(owner, name) :+ owner).sorted,
- getMilestones(owner, name),
+ getMilestonesWithIssueCount(owner, name),
getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount),
repository)
@@ -112,7 +112,7 @@
// record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
- redirect("/%s/%s/issues/%d".format(owner, name, issueId))
+ redirect(s"/${owner}/${name}/issues/${issueId}")
})
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
@@ -122,17 +122,21 @@
getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){
updateIssue(owner, name, issue.issueId, form.title, form.content)
- redirect("/%s/%s/issues/_data/%d".format(owner, name, issue.issueId))
+ redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized
} getOrElse NotFound
})
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
- handleComment(form.issueId, Some(form.content), repository)
+ handleComment(form.issueId, Some(form.content), repository)() map { id =>
+ redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}")
+ } getOrElse NotFound
})
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
- handleComment(form.issueId, form.content, repository)
+ handleComment(form.issueId, form.content, repository)() map { id =>
+ redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}")
+ } getOrElse NotFound
})
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
@@ -142,7 +146,7 @@
getComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){
updateComment(comment.commentId, form.content)
- redirect("/%s/%s/issue_comments/_data/%d".format(owner, name, comment.commentId))
+ redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
} else Unauthorized
} getOrElse NotFound
})
@@ -197,79 +201,81 @@
})
ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
- updateAssignedUserName(repository.owner, repository.name, params("id").toInt,
- params.get("assignedUserName") filter (_.trim != ""))
+ updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName"))
Ok("updated")
})
ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository =>
- updateMilestoneId(repository.owner, repository.name, params("id").toInt,
- params.get("milestoneId") collect { case x if x.trim != "" => x.toInt })
- Ok("updated")
+ updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
+ milestoneId("milestoneId").map { milestoneId =>
+ getMilestonesWithIssueCount(repository.owner, repository.name)
+ .find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
+ issues.milestones.html.progress(openCount + closeCount, closeCount, false)
+ } getOrElse NotFound
+ } getOrElse Ok()
})
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
- val owner = repository.owner
- val name = repository.name
- val userName = context.loginAccount.get.userName
+ val action = params.get("value")
- params.get("value") collect {
- case s if s == "close" => (s.capitalize, Some(s), true)
- case s if s == "reopen" => (s.capitalize, Some(s), false)
- } map { case (content, action, closed) =>
- params("checked").split(',') foreach { issueId =>
- createComment(owner, name, userName, issueId.toInt, content, action)
- updateClosed(owner, name, issueId.toInt, closed)
- }
- redirect("/%s/%s/issues".format(owner, name))
- } getOrElse NotFound
+ executeBatch(repository) {
+ handleComment(_, None, repository)( _ => action)
+ }
})
post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
- val owner = repository.owner
- val name = repository.name
+ val labelId = params("value").toInt
- params.get("value").map(_.toInt) map { labelId =>
- params("checked").split(',') foreach { issueId =>
- getIssueLabel(owner, name, issueId.toInt, labelId) getOrElse {
- registerIssueLabel(owner, name, issueId.toInt, labelId)
- }
+ executeBatch(repository) { issueId =>
+ getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
+ registerIssueLabel(repository.owner, repository.name, issueId, labelId)
}
- redirect("/%s/%s/issues".format(owner, name))
- } getOrElse NotFound
+ }
})
post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
- params("checked").split(',') foreach { issueId =>
- updateAssignedUserName(repository.owner, repository.name, issueId.toInt,
- params.get("value") filter (_.trim != ""))
+ val value = assignedUserName("value")
+
+ executeBatch(repository) {
+ updateAssignedUserName(repository.owner, repository.name, _, value)
}
- redirect("/%s/%s/issues".format(repository.owner, repository.name))
})
post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository =>
- params("checked").split(',') foreach { issueId =>
- updateMilestoneId(repository.owner, repository.name, issueId.toInt,
- params.get("value") collect { case x if x.trim != "" => x.toInt })
+ val value = milestoneId("value")
+
+ executeBatch(repository) {
+ updateMilestoneId(repository.owner, repository.name, _, value)
}
- redirect("/%s/%s/issues".format(repository.owner, repository.name))
})
+ val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
+ val milestoneId = (key: String) => params.get(key) collect { case x if x.trim != "" => x.toInt }
+
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
- private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo) = {
+ private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
+ params("checked").split(',') map(_.toInt) foreach execute
+ redirect(s"/${repository.owner}/${repository.name}/issues")
+ }
+
+ /**
+ * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
+ */
+ private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
+ (getAction: model.Issue => Option[String] =
+ p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
val owner = repository.owner
val name = repository.name
val userName = context.loginAccount.get.userName
getIssue(owner, name, issueId.toString) map { issue =>
val (action, recordActivity) =
- params.get("action")
- .filter(_ => isEditable(owner, name, issue.openedUserName))
+ getAction(issue)
.collect {
- case s if s == "close" => true -> (Some(s) -> Some(recordCloseIssueActivity _))
- case s if s == "reopen" => false -> (Some(s) -> Some(recordReopenIssueActivity _))
+ case "close" => true -> (Some("close") -> Some(recordCloseIssueActivity _))
+ case "reopen" => false -> (Some("reopen") -> Some(recordReopenIssueActivity _))
}
.map { case (closed, t) =>
updateClosed(owner, name, issueId, closed)
@@ -277,21 +283,26 @@
}
.getOrElse(None -> None)
- val commentId = createComment(owner, name, userName, issueId, content.getOrElse(action.get.capitalize), action)
+ val commentId = content
+ .map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") )
+ .getOrElse ( action.get.capitalize -> action.get )
+ match {
+ case (content, action) => createComment(owner, name, userName, issueId, content, action)
+ }
// record activity
content foreach ( recordCommentIssueActivity(owner, name, userName, issueId, _) )
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
- redirect("/%s/%s/issues/%d#comment-%d".format(owner, name, issueId, commentId))
- } getOrElse NotFound
+ commentId
+ }
}
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
val owner = repository.owner
val repoName = repository.name
val userName = if(filter != "all") Some(params("userName")) else None
- val sessionKey = "%s/%s/issues".format(owner, repoName)
+ val sessionKey = s"${owner}/${repoName}/issues"
val page = try {
val i = params.getOrElse("page", "1").toInt
@@ -311,7 +322,7 @@
searchIssue(owner, repoName, condition, filter, userName, (page - 1) * IssueLimit, IssueLimit),
page,
(getCollaborators(owner, repoName) :+ owner).sorted,
- getMilestones(owner, repoName).filter(_.closedDate.isEmpty),
+ getMilestones(owner, repoName),
getLabels(owner, repoName),
countIssue(owner, repoName, condition.copy(state = "open"), filter, userName),
countIssue(owner, repoName, condition.copy(state = "closed"), filter, userName),
diff --git a/src/main/scala/app/MilestonesController.scala b/src/main/scala/app/MilestonesController.scala
index 55c60d0..f4ae4cb 100644
--- a/src/main/scala/app/MilestonesController.scala
+++ b/src/main/scala/app/MilestonesController.scala
@@ -3,7 +3,7 @@
import jp.sf.amateras.scalatra.forms._
import service._
-import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, UsersAuthenticator}
+import util.{CollaboratorsAuthenticator, ReferrerAuthenticator}
class MilestonesController extends MilestonesControllerBase
with MilestonesService with RepositoryService with AccountService
diff --git a/src/main/scala/app/RepositorySettingsController.scala b/src/main/scala/app/RepositorySettingsController.scala
index 08f6474..780a7f3 100644
--- a/src/main/scala/app/RepositorySettingsController.scala
+++ b/src/main/scala/app/RepositorySettingsController.scala
@@ -54,22 +54,19 @@
* Display the Collaborators page.
*/
get("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
- settings.html.collaborators(getCollaborators(repository.owner, repository.name), repository)
- })
-
- /**
- * JSON API for collaborator completion.
- */
- get("/:owner/:repository/settings/collaborators/proposals")(usersOnly {
- contentType = formats("json")
- org.json4s.jackson.Serialization.write(Map("options" -> getAllUsers.map(_.userName).toArray))
+ settings.html.collaborators(
+ getCollaborators(repository.owner, repository.name),
+ getAccountByUserName(repository.owner).get.isGroupAccount,
+ repository)
})
/**
* Add the collaborator.
*/
post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) =>
- addCollaborator(repository.owner, repository.name, form.userName)
+ if(!getAccountByUserName(repository.owner).get.isGroupAccount){
+ addCollaborator(repository.owner, repository.name, form.userName)
+ }
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
})
@@ -77,7 +74,9 @@
* Add the collaborator.
*/
get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository =>
- removeCollaborator(repository.owner, repository.name, params("name"))
+ if(!getAccountByUserName(repository.owner).get.isGroupAccount){
+ removeCollaborator(repository.owner, repository.name, params("name"))
+ }
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
})
diff --git a/src/main/scala/app/SearchController.scala b/src/main/scala/app/SearchController.scala
new file mode 100644
index 0000000..38bf35a
--- /dev/null
+++ b/src/main/scala/app/SearchController.scala
@@ -0,0 +1,51 @@
+package app
+
+import util._
+import service._
+import jp.sf.amateras.scalatra.forms._
+
+class SearchController extends SearchControllerBase
+with RepositoryService with AccountService with SystemSettingsService with ActivityService
+with RepositorySearchService with IssuesService
+with ReferrerAuthenticator
+
+trait SearchControllerBase extends ControllerBase { self: RepositoryService
+ with SystemSettingsService with ActivityService with RepositorySearchService
+ with ReferrerAuthenticator =>
+
+ val searchForm = mapping(
+ "query" -> trim(text(required)),
+ "owner" -> trim(text(required)),
+ "repository" -> trim(text(required))
+ )(SearchForm.apply)
+
+ case class SearchForm(query: String, owner: String, repository: String)
+
+ post("/search", searchForm){ form =>
+ redirect(s"${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}")
+ }
+
+ get("/:owner/:repository/search")(referrersOnly { repository =>
+ val query = params("q").trim
+ val target = params.getOrElse("type", "code")
+ val page = try {
+ val i = params.getOrElse("page", "1").toInt
+ if(i <= 0) 1 else i
+ } catch {
+ case e: NumberFormatException => 1
+ }
+
+ target.toLowerCase match {
+ case "issue" => search.html.issues(
+ searchIssues(repository.owner, repository.name, query),
+ countFiles(repository.owner, repository.name, query),
+ query, page, repository)
+
+ case _ => search.html.code(
+ searchFiles(repository.owner, repository.name, query),
+ countIssues(repository.owner, repository.name, query),
+ query, page, repository)
+ }
+ })
+
+}
diff --git a/src/main/scala/app/SignInController.scala b/src/main/scala/app/SignInController.scala
index becc3b2..a94609a 100644
--- a/src/main/scala/app/SignInController.scala
+++ b/src/main/scala/app/SignInController.scala
@@ -24,20 +24,19 @@
}
post("/signin", form){ form =>
- val account = getAccountByUserName(form.userName)
- if(account.isEmpty || account.get.password != sha1(form.password)){
- redirect("/signin")
- } else {
- session.setAttribute("LOGIN_ACCOUNT", account.get)
- updateLastLoginDate(account.get.userName)
+ getAccountByUserName(form.userName).collect {
+ case account if(!account.isGroupAccount && account.password == sha1(form.password)) => {
+ session.setAttribute("LOGIN_ACCOUNT", account)
+ updateLastLoginDate(account.userName)
- session.get("REDIRECT").map { redirectUrl =>
- session.removeAttribute("REDIRECT")
- redirect(redirectUrl.asInstanceOf[String])
- }.getOrElse {
- redirect("/")
+ session.get("REDIRECT").map { redirectUrl =>
+ session.removeAttribute("REDIRECT")
+ redirect(redirectUrl.asInstanceOf[String])
+ }.getOrElse {
+ redirect("/")
+ }
}
- }
+ } getOrElse redirect("/signin")
}
get("/signout"){
diff --git a/src/main/scala/app/UserManagementController.scala b/src/main/scala/app/UserManagementController.scala
index e5673d4..08f14d8 100644
--- a/src/main/scala/app/UserManagementController.scala
+++ b/src/main/scala/app/UserManagementController.scala
@@ -1,34 +1,38 @@
package app
import service._
-import util.{FileUploadUtil, FileUtil, AdminAuthenticator}
+import util.AdminAuthenticator
import util.StringUtil._
import jp.sf.amateras.scalatra.forms._
-import org.apache.commons.io.FileUtils
-import util.Directory._
-import scala.Some
-class UserManagementController extends UserManagementControllerBase with AccountService with AdminAuthenticator
+class UserManagementController extends UserManagementControllerBase
+ with AccountService with RepositoryService with AdminAuthenticator
trait UserManagementControllerBase extends AccountManagementControllerBase {
- self: AccountService with AdminAuthenticator =>
+ self: AccountService with RepositoryService with AdminAuthenticator =>
- case class UserNewForm(userName: String, password: String, mailAddress: String, isAdmin: Boolean,
+ case class NewUserForm(userName: String, password: String, mailAddress: String, isAdmin: Boolean,
url: Option[String], fileId: Option[String])
- case class UserEditForm(userName: String, password: Option[String], mailAddress: String, isAdmin: Boolean,
+ case class EditUserForm(userName: String, password: Option[String], mailAddress: String, isAdmin: Boolean,
url: Option[String], fileId: Option[String], clearImage: Boolean)
- val newForm = mapping(
+ case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
+ memberNames: Option[String])
+
+ case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
+ memberNames: Option[String], clearImage: Boolean)
+
+ val newUserForm = mapping(
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" , text(required, maxlength(20)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
"isAdmin" -> trim(label("User Type" , boolean())),
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text())))
- )(UserNewForm.apply)
+ )(NewUserForm.apply)
- val editForm = mapping(
+ val editUserForm = mapping(
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier))),
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
@@ -36,28 +40,47 @@
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))),
"clearImage" -> trim(label("Clear image" , boolean()))
- )(UserEditForm.apply)
-
+ )(EditUserForm.apply)
+
+ val newGroupForm = 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())))
+ )(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()))
+ )(EditGroupForm.apply)
+
get("/admin/users")(adminOnly {
- admin.users.html.list(getAllUsers())
+ val users = getAllUsers()
+ val members = users.collect { case account if(account.isGroupAccount) =>
+ account.userName -> getGroupMembers(account.userName)
+ }.toMap
+ admin.users.html.list(users, members)
})
- get("/admin/users/_new")(adminOnly {
- admin.users.html.edit(None)
+ get("/admin/users/_newuser")(adminOnly {
+ admin.users.html.user(None)
})
- post("/admin/users/_new", newForm)(adminOnly { form =>
+ post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
createAccount(form.userName, sha1(form.password), form.mailAddress, form.isAdmin, form.url)
updateImage(form.userName, form.fileId, false)
redirect("/admin/users")
})
- get("/admin/users/:userName/_edit")(adminOnly {
+ get("/admin/users/:userName/_edituser")(adminOnly {
val userName = params("userName")
- admin.users.html.edit(getAccountByUserName(userName))
+ admin.users.html.user(getAccountByUserName(userName))
})
- post("/admin/users/:name/_edit", editForm)(adminOnly { form =>
+ post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
val userName = params("userName")
getAccountByUserName(userName).map { account =>
updateAccount(getAccountByUserName(userName).get.copy(
@@ -71,5 +94,46 @@
} getOrElse NotFound
})
-
+
+ get("/admin/users/_newgroup")(adminOnly {
+ admin.users.html.group(None, Nil)
+ })
+
+ post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
+ createGroup(form.groupName, form.url)
+ updateGroupMembers(form.groupName, form.memberNames.map(_.split(",").toList).getOrElse(Nil))
+ updateImage(form.groupName, form.fileId, false)
+ redirect("/admin/users")
+ })
+
+ get("/admin/users/:groupName/_editgroup")(adminOnly {
+ val groupName = params("groupName")
+ admin.users.html.group(getAccountByUserName(groupName), getGroupMembers(groupName))
+ })
+
+ post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
+ val groupName = params("groupName")
+ getAccountByUserName(groupName).map { account =>
+ updateGroup(groupName, form.url)
+
+ val memberNames = form.memberNames.map(_.split(",").toList).getOrElse(Nil)
+ updateGroupMembers(form.groupName, memberNames)
+
+ getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
+ removeCollaborators(form.groupName, repositoryName)
+ memberNames.foreach { userName =>
+ addCollaborator(form.groupName, repositoryName, userName)
+ }
+ }
+
+ updateImage(form.groupName, form.fileId, form.clearImage)
+ redirect("/admin/users")
+
+ } getOrElse NotFound
+ })
+
+ post("/admin/users/_usercheck")(adminOnly {
+ getAccountByUserName(params("userName")).isDefined
+ })
+
}
\ No newline at end of file
diff --git a/src/main/scala/model/Account.scala b/src/main/scala/model/Account.scala
index d0f1f59..86ff251 100644
--- a/src/main/scala/model/Account.scala
+++ b/src/main/scala/model/Account.scala
@@ -12,7 +12,8 @@
def updatedDate = column[java.util.Date]("UPDATED_DATE")
def lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE")
def image = column[String]("IMAGE")
- def * = userName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? <> (Account, Account.unapply _)
+ def groupAccount = column[Boolean]("GROUP_ACCOUNT")
+ def * = userName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? ~ groupAccount <> (Account, Account.unapply _)
}
case class Account(
@@ -24,5 +25,6 @@
registeredDate: java.util.Date,
updatedDate: java.util.Date,
lastLoginDate: Option[java.util.Date],
- image: Option[String]
+ image: Option[String],
+ isGroupAccount: Boolean
)
diff --git a/src/main/scala/model/GroupMembers.scala b/src/main/scala/model/GroupMembers.scala
new file mode 100644
index 0000000..0bcd0af
--- /dev/null
+++ b/src/main/scala/model/GroupMembers.scala
@@ -0,0 +1,14 @@
+package model
+
+import scala.slick.driver.H2Driver.simple._
+
+object GroupMembers extends Table[GroupMember]("GROUP_MEMBER") {
+ def groupName = column[String]("GROUP_NAME", O PrimaryKey)
+ def userName = column[String]("USER_NAME", O PrimaryKey)
+ def * = groupName ~ userName <> (GroupMember, GroupMember.unapply _)
+}
+
+case class GroupMember(
+ groupName: String,
+ userName: String
+)
\ No newline at end of file
diff --git a/src/main/scala/model/IssueComment.scala b/src/main/scala/model/IssueComment.scala
index 1084a33..f9fd983 100644
--- a/src/main/scala/model/IssueComment.scala
+++ b/src/main/scala/model/IssueComment.scala
@@ -9,9 +9,9 @@
def content = column[String]("CONTENT")
def registeredDate = column[java.util.Date]("REGISTERED_DATE")
def updatedDate = column[java.util.Date]("UPDATED_DATE")
- def * = userName ~ repositoryName ~ issueId ~ commentId ~ action.? ~ commentedUserName ~ content ~ registeredDate ~ updatedDate <> (IssueComment, IssueComment.unapply _)
+ def * = userName ~ repositoryName ~ issueId ~ commentId ~ action ~ commentedUserName ~ content ~ registeredDate ~ updatedDate <> (IssueComment, IssueComment.unapply _)
- def autoInc = userName ~ repositoryName ~ issueId ~ action.? ~ commentedUserName ~ content ~ registeredDate ~ updatedDate returning commentId
+ def autoInc = userName ~ repositoryName ~ issueId ~ action ~ commentedUserName ~ content ~ registeredDate ~ updatedDate returning commentId
def byPrimaryKey(commentId: Int) = this.commentId is commentId.bind
}
@@ -20,7 +20,7 @@
repositoryName: String,
issueId: Int,
commentId: Int,
- action: Option[String],
+ action: String,
commentedUserName: String,
content: String,
registeredDate: java.util.Date,
diff --git a/src/main/scala/model/package.scala b/src/main/scala/model/package.scala
index 475da12..3280c35 100644
--- a/src/main/scala/model/package.scala
+++ b/src/main/scala/model/package.scala
@@ -1,5 +1,6 @@
package object model {
- import scala.slick.lifted.MappedTypeMapper
+ import scala.slick.driver.BasicDriver.Implicit._
+ import scala.slick.lifted.{Column, MappedTypeMapper}
// java.util.Date TypeMapper
implicit val dateTypeMapper = MappedTypeMapper.base[java.util.Date, java.sql.Timestamp](
@@ -7,6 +8,10 @@
t => new java.util.Date(t.getTime)
)
+ implicit class RichColumn(c1: Column[Boolean]){
+ def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
+ }
+
/**
* Returns system date.
*/
diff --git a/src/main/scala/service/AccountService.scala b/src/main/scala/service/AccountService.scala
index 28223e1..3f828e3 100644
--- a/src/main/scala/service/AccountService.scala
+++ b/src/main/scala/service/AccountService.scala
@@ -24,7 +24,8 @@
registeredDate = currentDate,
updatedDate = currentDate,
lastLoginDate = None,
- image = None)
+ image = None,
+ isGroupAccount = false)
def updateAccount(account: Account): Unit =
Accounts
@@ -44,5 +45,42 @@
def updateLastLoginDate(userName: String): Unit =
Accounts.filter(_.userName is userName.bind).map(_.lastLoginDate).update(currentDate)
-
+
+ def createGroup(groupName: String, url: Option[String]): Unit =
+ Accounts insert Account(
+ userName = groupName,
+ password = "",
+ mailAddress = groupName + "@devnull",
+ isAdmin = false,
+ url = url,
+ registeredDate = currentDate,
+ updatedDate = currentDate,
+ lastLoginDate = None,
+ image = None,
+ isGroupAccount = true)
+
+ def updateGroup(groupName: String, url: Option[String]): Unit =
+ Accounts.filter(_.userName is groupName.bind).map(_.url.?).update(url)
+
+ def updateGroupMembers(groupName: String, members: List[String]): Unit = {
+ Query(GroupMembers).filter(_.groupName is groupName.bind).delete
+ members.foreach { userName =>
+ GroupMembers insert GroupMember (groupName, userName)
+ }
+ }
+
+ def getGroupMembers(groupName: String): List[String] =
+ Query(GroupMembers)
+ .filter(_.groupName is groupName.bind)
+ .sortBy(_.userName)
+ .map(_.userName)
+ .list
+
+ def getGroupsByUserName(userName: String): List[String] =
+ Query(GroupMembers)
+ .filter(_.userName is userName.bind)
+ .sortBy(_.groupName)
+ .map(_.groupName)
+ .list
+
}
diff --git a/src/main/scala/service/IssuesService.scala b/src/main/scala/service/IssuesService.scala
index 17f3c69..c0f90e0 100644
--- a/src/main/scala/service/IssuesService.scala
+++ b/src/main/scala/service/IssuesService.scala
@@ -6,8 +6,8 @@
import Q.interpolation
import model._
-import util.StringUtil._
import util.Implicits._
+import util.StringUtil
trait IssuesService {
import IssuesService._
@@ -102,7 +102,10 @@
// get issues and comment count
val issues = searchIssueQuery(owner, repository, condition, filter, userName)
.leftJoin(Query(IssueComments)
- .filter { _.byRepository(owner, repository) }
+ .filter { t =>
+ (t.byRepository(owner, repository)) &&
+ (t.action inSetBind Seq("comment", "close_comment", "reopen_comment"))
+ }
.groupBy { _.issueId }
.map { case (issueId, t) => issueId ~ t.length }).on((t1, t2) => t1.issueId is t2._1)
.sortBy { case (t1, t2) =>
@@ -192,7 +195,7 @@
IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete
def createComment(owner: String, repository: String, loginUser: String,
- issueId: Int, content: String, action: Option[String]) =
+ issueId: Int, content: String, action: String) =
IssueComments.autoInc insert (
owner,
repository,
@@ -234,10 +237,60 @@
}
.update (closed, currentDate)
+ /**
+ * Search issues by keyword.
+ *
+ * @param owner the repository owner
+ * @param repository the repository name
+ * @param query the keywords separated by whitespace.
+ * @return issues with comment count and matched content of issue or comment
+ */
+ def searchIssuesByKeyword(owner: String, repository: String, query: String): List[(Issue, Int, String)] = {
+ import scala.slick.driver.H2Driver.likeEncode
+ val keywords = StringUtil.splitWords(query.toLowerCase)
+
+ // Search Issue
+ val issues = Query(Issues).filter { t =>
+ keywords.map { keyword =>
+ (t.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) ||
+ (t.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^'))
+ } .reduceLeft(_ && _)
+ }.map { t => (t, 0, t.content.?) }
+
+ // Search IssueComment
+ val comments = Query(IssueComments).innerJoin(Issues).on { case (t1, t2) =>
+ t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
+ }.filter { case (t1, t2) =>
+ keywords.map { query =>
+ t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^')
+ }.reduceLeft(_ && _)
+ }.map { case (t1, t2) => (t2, t1.commentId, t1.content.?) }
+
+ def getCommentCount(issue: Issue): Int = {
+ Query(IssueComments)
+ .filter { t =>
+ t.byIssue(issue.userName, issue.repositoryName, issue.issueId) &&
+ (t.action inSetBind Seq("comment", "close_comment", "reopen_comment"))
+ }
+ .map(_.issueId)
+ .list.length
+ }
+
+ issues.union(comments).sortBy { case (issue, commentId, _) =>
+ issue.issueId ~ commentId
+ }.list.splitWith { case ((issue1, _, _), (issue2, _, _)) =>
+ issue1.issueId == issue2.issueId
+ }.map { result =>
+ val (issue, _, content) = result.head
+ (issue, getCommentCount(issue) , content.getOrElse(""))
+ }.toList
+ }
+
}
object IssuesService {
import javax.servlet.http.HttpServletRequest
+ import util.StringUtil._
val IssueLimit = 30
@@ -279,4 +332,5 @@
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"))
}
+
}
diff --git a/src/main/scala/service/RepositorySearchService.scala b/src/main/scala/service/RepositorySearchService.scala
new file mode 100644
index 0000000..669a36b
--- /dev/null
+++ b/src/main/scala/service/RepositorySearchService.scala
@@ -0,0 +1,121 @@
+package service
+
+import model.Issue
+import util.{FileUtil, StringUtil, JGitUtil}
+import util.Directory._
+import model.Issue
+import org.eclipse.jgit.revwalk.RevWalk
+import org.eclipse.jgit.treewalk.TreeWalk
+import scala.collection.mutable.ListBuffer
+import org.eclipse.jgit.lib.FileMode
+import org.eclipse.jgit.api.Git
+
+trait RepositorySearchService { self: IssuesService =>
+ import RepositorySearchService._
+
+ def countIssues(owner: String, repository: String, query: String): Int =
+ searchIssuesByKeyword(owner, repository, query).length
+
+ def searchIssues(owner: String, repository: String, query: String): List[IssueSearchResult] =
+ searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) =>
+ IssueSearchResult(
+ issue.issueId,
+ issue.title,
+ issue.openedUserName,
+ issue.registeredDate,
+ commentCount,
+ getHighlightText(content, query)._1)
+ }
+
+ def countFiles(owner: String, repository: String, query: String): Int =
+ JGitUtil.withGit(getRepositoryDir(owner, repository)){ git =>
+ searchRepositoryFiles(git, query).length
+ }
+
+ def searchFiles(owner: String, repository: String, query: String): List[FileSearchResult] =
+ JGitUtil.withGit(getRepositoryDir(owner, repository)){ git =>
+ val files = searchRepositoryFiles(git, query)
+ val commits = JGitUtil.getLatestCommitFromPaths(git, files.toList.map(_._1), "HEAD")
+ files.map { case (path, text) =>
+ val (highlightText, lineNumber) = getHighlightText(text, query)
+ FileSearchResult(
+ path,
+ commits(path).getCommitterIdent.getWhen,
+ highlightText,
+ lineNumber)
+ }
+ }
+
+ private def searchRepositoryFiles(git: Git, query: String): List[(String, String)] = {
+ val revWalk = new RevWalk(git.getRepository)
+ val objectId = git.getRepository.resolve("HEAD")
+ val revCommit = revWalk.parseCommit(objectId)
+ val treeWalk = new TreeWalk(git.getRepository)
+ treeWalk.setRecursive(true)
+ treeWalk.addTree(revCommit.getTree)
+
+ val keywords = StringUtil.splitWords(query.toLowerCase)
+ val list = new ListBuffer[(String, String)]
+
+ while (treeWalk.next()) {
+ if(treeWalk.getFileMode(0) != FileMode.TREE){
+ JGitUtil.getContent(git, treeWalk.getObjectId(0), false).foreach { bytes =>
+ if(FileUtil.isText(bytes)){
+ val text = new String(bytes, "UTF-8")
+ val lowerText = text.toLowerCase
+ val indices = keywords.map(lowerText.indexOf _)
+ if(!indices.exists(_ < 0)){
+ list.append((treeWalk.getPathString, text))
+ }
+ }
+ }
+ }
+ }
+ treeWalk.release
+ revWalk.release
+
+ list.toList
+ }
+
+}
+
+object RepositorySearchService {
+
+ val CodeLimit = 10
+ val IssueLimit = 10
+
+ def getHighlightText(content: String, query: String): (String, Int) = {
+ val keywords = StringUtil.splitWords(query.toLowerCase)
+ val lowerText = content.toLowerCase
+ val indices = keywords.map(lowerText.indexOf _)
+
+ if(!indices.exists(_ < 0)){
+ val lineNumber = content.substring(0, indices.min).split("\n").size - 1
+ val highlightText = StringUtil.escapeHtml(content.split("\n").drop(lineNumber).take(5).mkString("\n"))
+ .replaceAll("(?i)(" + keywords.map("\\Q" + _ + "\\E").mkString("|") + ")",
+ "$1")
+ (highlightText, lineNumber + 1)
+ } else {
+ (content.split("\n").take(5).mkString("\n"), 1)
+ }
+ }
+
+ case class SearchResult(
+ files : List[(String, String)],
+ issues: List[(Issue, Int, String)])
+
+ case class IssueSearchResult(
+ issueId: Int,
+ title: String,
+ openedUserName: String,
+ registeredDate: java.util.Date,
+ commentCount: Int,
+ highlightText: String)
+
+ case class FileSearchResult(
+ path: String,
+ lastModified: java.util.Date,
+ highlightText: String,
+ highlightLineNumber: Int)
+
+}
\ No newline at end of file
diff --git a/src/main/scala/service/RepositoryService.scala b/src/main/scala/service/RepositoryService.scala
index 4ec1651..b9ce2e2 100644
--- a/src/main/scala/service/RepositoryService.scala
+++ b/src/main/scala/service/RepositoryService.scala
@@ -167,6 +167,15 @@
Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete
/**
+ * Remove all collaborators from the repository.
+ *
+ * @param userName the user name of the repository owner
+ * @param repositoryName the repository name
+ */
+ def removeCollaborators(userName: String, repositoryName: String): Unit =
+ Collaborators.filter(_.byRepository(userName, repositoryName)).delete
+
+ /**
* Returns the list of collaborators name which is sorted with ascending order.
*
* @param userName the user name of the repository owner
diff --git a/src/main/scala/service/WikiService.scala b/src/main/scala/service/WikiService.scala
index 5cfe5b5..bb2542b 100644
--- a/src/main/scala/service/WikiService.scala
+++ b/src/main/scala/service/WikiService.scala
@@ -6,7 +6,6 @@
import org.apache.commons.io.FileUtils
import util.JGitUtil.DiffInfo
import util.{Directory, JGitUtil}
-import org.eclipse.jgit.lib.RepositoryBuilder
import org.eclipse.jgit.treewalk.CanonicalTreeParser
import java.util.concurrent.ConcurrentHashMap
@@ -64,16 +63,16 @@
trait WikiService {
import WikiService._
- def createWikiRepository(owner: model.Account, repository: String): Unit = {
- lock(owner.userName, repository){
- val dir = Directory.getWikiRepositoryDir(owner.userName, repository)
+ def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit = {
+ lock(owner, repository){
+ val dir = Directory.getWikiRepositoryDir(owner, repository)
if(!dir.exists){
try {
JGitUtil.initRepository(dir)
- saveWikiPage(owner.userName, repository, "Home", "Home", "Welcome to the %s wiki!!".format(repository), owner, "Initial Commit")
+ saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit")
} finally {
// once delete cloned repository because initial cloned repository does not have 'branch.master.merge'
- FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner.userName, repository))
+ FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner, repository))
}
}
}
@@ -119,10 +118,12 @@
* Returns the list of wiki page names.
*/
def getWikiPageList(owner: String, repository: String): List[String] = {
- JGitUtil.getFileList(Git.open(Directory.getWikiRepositoryDir(owner, repository)), "master", ".")
- .filter(_.name.endsWith(".md"))
- .map(_.name.replaceFirst("\\.md$", ""))
- .sortBy(x => x)
+ JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git =>
+ JGitUtil.getFileList(git, "master", ".")
+ .filter(_.name.endsWith(".md"))
+ .map(_.name.replaceFirst("\\.md$", ""))
+ .sortBy(x => x)
+ }
}
/**
@@ -189,12 +190,16 @@
private def cloneOrPullWorkingCopy(workDir: File, owner: String, repository: String): Unit = {
if(!workDir.exists){
- Git.cloneRepository
- .setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString)
- .setDirectory(workDir)
- .call
+ val git =
+ Git.cloneRepository
+ .setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString)
+ .setDirectory(workDir)
+ .call
+ git.getRepository.close // close .git resources.
} else {
- Git.open(workDir).pull.call
+ JGitUtil.withGit(workDir){ git =>
+ git.pull.call
+ }
}
}
diff --git a/src/main/scala/servlet/AutoUpdateListener.scala b/src/main/scala/servlet/AutoUpdateListener.scala
index 4fe7397..601af9b 100644
--- a/src/main/scala/servlet/AutoUpdateListener.scala
+++ b/src/main/scala/servlet/AutoUpdateListener.scala
@@ -129,6 +129,7 @@
} catch {
case ex: Throwable => {
logger.error("Failed to schema update", ex)
+ ex.printStackTrace()
conn.rollback()
}
}
diff --git a/src/main/scala/servlet/BasicAuthenticationFilter.scala b/src/main/scala/servlet/BasicAuthenticationFilter.scala
index a69c237..51d6618 100644
--- a/src/main/scala/servlet/BasicAuthenticationFilter.scala
+++ b/src/main/scala/servlet/BasicAuthenticationFilter.scala
@@ -32,7 +32,8 @@
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki", ""), "") match {
case Some(repository) => {
- if(!request.getRequestURI.endsWith("/git-receive-pack") && !repository.repository.isPrivate){
+ if(!request.getRequestURI.endsWith("/git-receive-pack") &&
+ !"service=git-receive-pack".equals(request.getQueryString) && !repository.repository.isPrivate){
chain.doFilter(req, wrappedResponse)
} else {
request.getHeader("Authorization") match {
diff --git a/src/main/scala/servlet/GitRepositoryServlet.scala b/src/main/scala/servlet/GitRepositoryServlet.scala
index 55ce0e9..94c432b 100644
--- a/src/main/scala/servlet/GitRepositoryServlet.scala
+++ b/src/main/scala/servlet/GitRepositoryServlet.scala
@@ -85,7 +85,7 @@
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData =>
val issueId = matchData.group(2)
if(getAccountByUserName(commit.committer).isDefined && getIssue(owner, repository, issueId).isDefined){
- createComment(owner, repository, commit.committer, issueId.toInt, commit.fullMessage, Some("commit"))
+ createComment(owner, repository, commit.committer, issueId.toInt, commit.fullMessage, "commit")
}
}
Some(commit)
diff --git a/src/main/scala/servlet/SessionCleanupListener.scala b/src/main/scala/servlet/SessionCleanupListener.scala
index 86c8bcc..87ce1d1 100644
--- a/src/main/scala/servlet/SessionCleanupListener.scala
+++ b/src/main/scala/servlet/SessionCleanupListener.scala
@@ -1,17 +1,15 @@
package servlet
-import util.FileUploadUtil
import javax.servlet.http.{HttpSessionEvent, HttpSessionListener}
+import app.FileUploadControllerBase
/**
* Removes session associated temporary files when session is destroyed.
*/
-class SessionCleanupListener extends HttpSessionListener {
+class SessionCleanupListener extends HttpSessionListener with FileUploadControllerBase {
def sessionCreated(se: HttpSessionEvent): Unit = {}
- def sessionDestroyed(se: HttpSessionEvent): Unit = {
- FileUploadUtil.removeTemporaryFiles()(se.getSession)
- }
+ def sessionDestroyed(se: HttpSessionEvent): Unit = removeTemporaryFiles()(se.getSession)
}
diff --git a/src/main/scala/util/Directory.scala b/src/main/scala/util/Directory.scala
index f2b7f4c..754d737 100644
--- a/src/main/scala/util/Directory.scala
+++ b/src/main/scala/util/Directory.scala
@@ -1,8 +1,6 @@
package util
import java.io.File
-import org.eclipse.jgit.api.Git
-import org.eclipse.jgit.lib.Ref
/**
* Provides directories used by GitBucket.
diff --git a/src/main/scala/util/FileUploadUtil.scala b/src/main/scala/util/FileUploadUtil.scala
deleted file mode 100644
index 6efacc1..0000000
--- a/src/main/scala/util/FileUploadUtil.scala
+++ /dev/null
@@ -1,33 +0,0 @@
-package util
-
-import java.text.SimpleDateFormat
-import javax.servlet.http.HttpSession
-import util.Directory._
-import org.apache.commons.io.FileUtils
-
-object FileUploadUtil {
-
- def generateFileId: String =
- new SimpleDateFormat("yyyyMMddHHmmSSsss").format(new java.util.Date(System.currentTimeMillis))
-
- def TemporaryDir(implicit session: HttpSession): java.io.File =
- new java.io.File(GitBucketHome, s"tmp/_upload/${session.getId}")
-
- def getTemporaryFile(fileId: String)(implicit session: HttpSession): java.io.File =
- new java.io.File(TemporaryDir, fileId)
-
-// def removeTemporaryFile(fileId: String)(implicit session: HttpSession): Unit =
-// getTemporaryFile(fileId).delete()
-
- def removeTemporaryFiles()(implicit session: HttpSession): Unit =
- FileUtils.deleteDirectory(TemporaryDir)
-
- def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] = {
- val filename = Option(session.getAttribute("upload_" + fileId).asInstanceOf[String])
- if(filename.isDefined){
- session.removeAttribute("upload_" + fileId)
- }
- filename
- }
-
-}
diff --git a/src/main/scala/util/FileUtil.scala b/src/main/scala/util/FileUtil.scala
index 01f8874..b56c7f4 100644
--- a/src/main/scala/util/FileUtil.scala
+++ b/src/main/scala/util/FileUtil.scala
@@ -1,6 +1,6 @@
package util
-import org.apache.commons.io.{IOUtils, FileUtils, FilenameUtils}
+import org.apache.commons.io.{IOUtils, FileUtils}
import java.net.URLConnection
import java.io.File
import org.apache.commons.compress.archivers.zip.{ZipArchiveEntry, ZipArchiveOutputStream}
diff --git a/src/main/scala/util/Implicits.scala b/src/main/scala/util/Implicits.scala
index c475136..388f9f6 100644
--- a/src/main/scala/util/Implicits.scala
+++ b/src/main/scala/util/Implicits.scala
@@ -1,6 +1,5 @@
package util
-import scala.slick.driver.H2Driver.simple._
import scala.util.matching.Regex
/**
@@ -25,11 +24,6 @@
}
}
- // TODO Should this implicit conversion move to model.Functions?
- implicit class RichColumn(c1: Column[Boolean]){
- def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
- }
-
implicit class RichString(value: String){
def replaceBy(regex: Regex)(replace: Regex.MatchData => Option[String]): String = {
val sb = new StringBuilder()
diff --git a/src/main/scala/util/JGitUtil.scala b/src/main/scala/util/JGitUtil.scala
index af71638..46d64e0 100644
--- a/src/main/scala/util/JGitUtil.scala
+++ b/src/main/scala/util/JGitUtil.scala
@@ -53,14 +53,21 @@
* @param id the commit id
* @param time the commit time
* @param committer the committer name
+ * @param mailAddress the mail address of the committer
* @param shortMessage the short message
* @param fullMessage the full message
* @param parents the list of parent commit id
*/
- case class CommitInfo(id: String, time: Date, committer: String, shortMessage: String, fullMessage: String, parents: List[String]){
+ case class CommitInfo(id: String, time: Date, committer: String, mailAddress: String,
+ shortMessage: String, fullMessage: String, parents: List[String]){
def this(rev: org.eclipse.jgit.revwalk.RevCommit) = this(
- rev.getName, rev.getCommitterIdent.getWhen, rev.getCommitterIdent.getName, rev.getShortMessage, rev.getFullMessage,
+ rev.getName,
+ rev.getCommitterIdent.getWhen,
+ rev.getCommitterIdent.getName,
+ rev.getCommitterIdent.getEmailAddress,
+ rev.getShortMessage,
+ rev.getFullMessage,
rev.getParents().map(_.name).toList)
val summary = {
diff --git a/src/main/scala/util/StringUtil.scala b/src/main/scala/util/StringUtil.scala
index ae6d868..d8ab997 100644
--- a/src/main/scala/util/StringUtil.scala
+++ b/src/main/scala/util/StringUtil.scala
@@ -20,4 +20,9 @@
def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8")
+ def splitWords(value: String): Array[String] = value.split("[ \\t ]+")
+
+ def escapeHtml(value: String): String =
+ value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)
+
}
diff --git a/src/main/scala/util/Validations.scala b/src/main/scala/util/Validations.scala
index 1d42d99..dd383e3 100644
--- a/src/main/scala/util/Validations.scala
+++ b/src/main/scala/util/Validations.scala
@@ -1,7 +1,6 @@
package util
import jp.sf.amateras.scalatra.forms._
-import scala.Some
trait Validations {
diff --git a/src/main/scala/view/AvatarImageProvider.scala b/src/main/scala/view/AvatarImageProvider.scala
index 33ac699..bd1703d 100644
--- a/src/main/scala/view/AvatarImageProvider.scala
+++ b/src/main/scala/view/AvatarImageProvider.scala
@@ -10,16 +10,21 @@
* Returns <img> which displays the avatar icon.
* Looks up Gravatar if avatar icon has not been configured in user settings.
*/
- protected def getAvatarImageHtml(userName: String, size: Int, tooltip: Boolean = false)(implicit context: app.Context): Html = {
+ protected def getAvatarImageHtml(userName: String, size: Int,
+ mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html = {
val src = getAccountByUserName(userName).collect { case account if(account.image.isEmpty) =>
s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress)}?s=${size}"""
} getOrElse {
- s"""${context.path}/${userName}/_avatar"""
+ if(mailAddress.nonEmpty){
+ s"""http://www.gravatar.com/avatar/${StringUtil.md5(mailAddress)}?s=${size}"""
+ } else {
+ s"""${context.path}/${userName}/_avatar"""
+ }
}
if(tooltip){
- Html(s"""""")
+ Html(s"""
""")
} else {
- Html(s"""
""")
+ Html(s"""
""")
}
}
diff --git a/src/main/scala/view/helpers.scala b/src/main/scala/view/helpers.scala
index 6dd257b..615ffab 100644
--- a/src/main/scala/view/helpers.scala
+++ b/src/main/scala/view/helpers.scala
@@ -40,7 +40,10 @@
* Looks up Gravatar if avatar icon has not been configured in user settings.
*/
def avatar(userName: String, size: Int, tooltip: Boolean = false)(implicit context: app.Context): Html =
- getAvatarImageHtml(userName, size, tooltip)
+ getAvatarImageHtml(userName, size, "", tooltip)
+
+ def avatar(commit: util.JGitUtil.CommitInfo, size: Int)(implicit context: app.Context): Html =
+ getAvatarImageHtml(commit.committer, size, commit.mailAddress)
/**
* Converts commit id, issue id and username to the link.
@@ -80,6 +83,7 @@
def assets(implicit context: app.Context): String =
s"${context.path}/assets"
+ def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime
/**
* Implicit conversion to add mkHtml() to Seq[Html].
diff --git a/src/main/twirl/account/activity.scala.html b/src/main/twirl/account/activity.scala.html
index 29b2982..1f2cefc 100644
--- a/src/main/twirl/account/activity.scala.html
+++ b/src/main/twirl/account/activity.scala.html
@@ -1,23 +1,6 @@
-@(account: model.Account, activities: List[model.Activity])(implicit context: app.Context)
+@(account: model.Account, groupNames: List[String], activities: List[model.Activity])(implicit context: app.Context)
@import context._
@import view.helpers._
-@html.main(account.userName){
-
@avatar(account.userName, 20)
@account.userName
- @if(account.isAdmin){
- (Administrator)
+ @if(account.isGroupAccount){
+ (Group)
} else {
- (Normal)
+ @if(account.isAdmin){
+ (Administrator)
+ } else {
+ (Normal)
+ }
+ }
+ @if(account.isGroupAccount){
+ @members(account.userName).map { userName =>
+ @avatar(userName, 20, tooltip = true)
+ }
}
- @account.mailAddress + @if(!account.isGroupAccount){ + @account.mailAddress + } @account.url.map { url => @url } @@ -32,7 +48,9 @@
Registered: @datetime(account.registeredDate)
Updated: @datetime(account.updatedDate)
- Last Login: @account.lastLoginDate.map(datetime)
+ @if(!account.isGroupAccount){
+ Last Login: @account.lastLoginDate.map(datetime)
+ }
|