Merge branch 'master' into fork-and-pullreq

Conflicts:
	src/main/resources/update/1_3.sql
	src/main/resources/update/1_4.sql
	src/main/scala/app/CreateRepositoryController.scala
	src/main/scala/service/WikiService.scala
	src/main/twirl/account/repositories.scala.html
This commit is contained in:
takezoe
2013-07-24 13:29:23 +09:00
82 changed files with 1693 additions and 547 deletions

View File

@@ -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';

View File

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

View File

@@ -5,6 +5,7 @@ import javax.servlet._
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, "/*")

View File

@@ -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 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
*/
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
}

View File

@@ -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 org.apache.commons.io.FileUtils
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 @@ abstract class ControllerBase extends ScalatraFilter
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 @@ case class Context(path: String, loginAccount: Option[Account], currentUrl: Stri
/**
* 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 @@ trait AccountManagementControllerBase extends ControllerBase { self: AccountServ
}
} 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 @@ trait AccountManagementControllerBase extends ControllerBase { self: AccountServ
.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
}
}

View File

@@ -5,9 +5,9 @@ import util.{JGitUtil, UsersAuthenticator, ReferrerAuthenticator}
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 @@ class CreateRepositoryController extends CreateRepositoryControllerBase
* 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 @@ trait CreateRepositoryControllerBase extends ControllerBase {
* 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 @@ trait CreateRepositoryControllerBase extends ControllerBase {
}
// 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 @@ trait CreateRepositoryControllerBase extends ControllerBase {
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.")
}
}
}

View File

@@ -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 @@ import org.apache.commons.io.FileUtils
* 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)
}

View File

@@ -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 with AccountService
with UsersAuthenticator =>
trait IndexControllerBase extends ControllerBase { self: RepositoryService
with SystemSettingsService with ActivityService =>
get("/"){
val loginAccount = context.loginAccount
@@ -18,4 +22,14 @@ trait IndexControllerBase extends ControllerBase { self: RepositoryService
)
}
}
/**
* 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))
})
}

View File

@@ -67,7 +67,7 @@ trait IssuesControllerBase extends ControllerBase {
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 @@ trait IssuesControllerBase extends ControllerBase {
// 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 @@ trait IssuesControllerBase extends ControllerBase {
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 @@ trait IssuesControllerBase extends ControllerBase {
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 @@ trait IssuesControllerBase extends ControllerBase {
})
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 @@ trait IssuesControllerBase extends ControllerBase {
}
.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 @@ trait IssuesControllerBase extends ControllerBase {
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),

View File

@@ -3,7 +3,7 @@ package app
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

View File

@@ -54,22 +54,19 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
* 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 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
* 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")
})

View File

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

View File

@@ -24,20 +24,19 @@ trait SignInControllerBase extends ControllerBase { self: SystemSettingsService
}
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"){

View File

@@ -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 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
"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 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
} 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
})
}

View File

@@ -12,7 +12,8 @@ object Accounts extends Table[Account]("ACCOUNT") {
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 @@ case class Account(
registeredDate: java.util.Date,
updatedDate: java.util.Date,
lastLoginDate: Option[java.util.Date],
image: Option[String]
image: Option[String],
isGroupAccount: Boolean
)

View File

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

View File

@@ -9,9 +9,9 @@ object IssueComments extends Table[IssueComment]("ISSUE_COMMENT") with IssueTemp
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 @@ case class IssueComment(
repositoryName: String,
issueId: Int,
commentId: Int,
action: Option[String],
action: String,
commentedUserName: String,
content: String,
registeredDate: java.util.Date,

View File

@@ -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 @@ package object model {
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.
*/

View File

@@ -24,7 +24,8 @@ trait AccountService {
registeredDate = currentDate,
updatedDate = currentDate,
lastLoginDate = None,
image = None)
image = None,
isGroupAccount = false)
def updateAccount(account: Account): Unit =
Accounts
@@ -44,5 +45,42 @@ trait AccountService {
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
}

View File

@@ -6,8 +6,8 @@ import scala.slick.jdbc.{StaticQuery => Q}
import Q.interpolation
import model._
import util.StringUtil._
import util.Implicits._
import util.StringUtil
trait IssuesService {
import IssuesService._
@@ -102,7 +102,10 @@ trait IssuesService {
// 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 @@ trait IssuesService {
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 @@ trait IssuesService {
}
.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 @@ object IssuesService {
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"))
}
}

View File

@@ -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("|") + ")",
"<span style=\"background-color: #ffff88;;\">$1</span>")
(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)
}

View File

@@ -166,6 +166,15 @@ trait RepositoryService { self: AccountService =>
def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String): Unit =
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.
*

View File

@@ -6,7 +6,6 @@ import org.eclipse.jgit.api.Git
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 @@ object WikiService {
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 @@ trait WikiService {
* 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 @@ trait WikiService {
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
}
}
}

View File

@@ -129,6 +129,7 @@ class AutoUpdateListener extends org.h2.server.web.DbStarter {
} catch {
case ex: Throwable => {
logger.error("Failed to schema update", ex)
ex.printStackTrace()
conn.rollback()
}
}

View File

@@ -32,7 +32,8 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
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 {

View File

@@ -85,7 +85,7 @@ class CommitLogHook(owner: String, repository: String, userName: String) extends
"(^|\\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)

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
package util
import scala.slick.driver.H2Driver.simple._
import scala.util.matching.Regex
/**
@@ -25,11 +24,6 @@ object Implicits {
}
}
// 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()

View File

@@ -53,14 +53,21 @@ object JGitUtil {
* @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 = {

View File

@@ -20,4 +20,9 @@ object StringUtil {
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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
}

View File

@@ -1,7 +1,6 @@
package util
import jp.sf.amateras.scalatra.forms._
import scala.Some
trait Validations {

View File

@@ -10,16 +10,21 @@ trait AvatarImageProvider { self: RequestCache =>
* Returns &lt;img&gt; 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"""<img src=${src} class="avatar" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title=${userName}/>""")
Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title="${userName}"/>""")
} else {
Html(s"""<img src=${src} class="avatar" style="width: ${size}px; height: ${size}px;" />""")
Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" />""")
}
}

View File

@@ -40,7 +40,10 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
* 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 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
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].

View File

@@ -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){
<div class="container-fluid">
<div class="row-fluid">
<div class="span4">
<div class="block">
<div class="account-image">@avatar(account.userName, 200)</div>
<div class="block-header">@account.userName</div>
</div>
<div class="block">
<div><i class="icon-home"></i> <a href="@account.url">@account.url</a></div>
<div><i class="icon-time"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
</div>
</div>
<div class="span8">
@tab(account, "activity")
@helper.html.activities(activities)
</div>
</div>
</div>
@main(account, groupNames, "activity"){
@helper.html.activities(activities)
}

View File

@@ -0,0 +1,48 @@
@(account: model.Account, groupNames: List[String], active: String)(body: Html)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(account.userName){
<div class="container-fluid">
<div class="row-fluid">
<div class="span4">
<div class="block">
<div class="account-image">@avatar(account.userName, 200)</div>
<div class="block-header">@account.userName</div>
</div>
<div class="block">
@if(account.url.isDefined){
<div><i class="icon-home"></i> <a href="@account.url">@account.url</a></div>
}
<div><i class="icon-time"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
</div>
@if(groupNames.nonEmpty){
<div>
<div>Groups</div>
@groupNames.map { groupName =>
<a href="@url(groupName)">@avatar(groupName, 36, tooltip = true)</a>
}
</div>
}
</div>
<div class="span8">
<ul class="nav nav-tabs">
<li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
@if(account.isGroupAccount){
<li@if(active == "members"){ class="active"}><a href="@url(account.userName)?tab=members">Members</a></li>
} else {
<li@if(active == "activity"){ class="active"}><a href="@url(account.userName)?tab=activity">Public Activity</a></li>
}
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
<li class="pull-right">
<div class="button-group">
<a href="@url(account.userName)/_edit" class="btn">Edit Your Profile</a>
</div>
</li>
}
</ul>
@body
</div>
</div>
</div>
}

View File

@@ -0,0 +1,16 @@
@(account: model.Account, members: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@main(account, Nil, "members"){
@if(members.isEmpty){
No members
} else {
@members.map { userName =>
<div class="block">
<div class="block-header">
@avatar(userName, 20) <a href="@url(userName)">@userName</a>
</div>
</div>
}
}
}

View File

@@ -1,45 +1,28 @@
@(account: model.Account, repositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
@(account: model.Account, groupNames: List[String], repositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(account.userName){
<div class="container-fluid">
<div class="row-fluid">
<div class="span4">
<div class="block">
<div class="account-image">@avatar(account.userName, 200)</div>
<div class="block-header">@account.userName</div>
</div>
<div class="block">
<div><i class="icon-home"></i> <a href="@account.url">@account.url</a></div>
<div><i class="icon-time"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
</div>
</div>
<div class="span8">
@tab(account, "repositories")
@if(repositories.isEmpty){
No repositories
} else {
@repositories.map { repository =>
<div class="block">
<div class="block-header">
<a href="@url(repository.owner)">@repository.owner</a>
/
<a href="@url(repository)">@repository.name</a>
@if(repository.repository.isPrivate){
<i class="icon-lock"></i>
}
</div>
@if(repository.repository.originUserName.isDefined){
<div class="small muted">forked from <a href="@path/@repository.repository.originUserName/@repository.repository.originRepositoryName">@repository.repository.originUserName/@repository.repository.originRepositoryName</a></div>
}
@if(repository.repository.description.isDefined){
<div>@repository.repository.description</div>
}
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
</div>
@main(account, groupNames, "repositories"){
@if(repositories.isEmpty){
No repositories
} else {
@repositories.map { repository =>
<div class="block">
<div class="block-header">
<a href="@url(repository.owner)">@repository.owner</a>
/
<a href="@url(repository)">@repository.name</a>
@if(repository.repository.isPrivate){
<i class="icon-lock"></i>
}
</div>
@if(repository.repository.originUserName.isDefined){
<div class="small muted">forked from <a href="@path/@repository.repository.originUserName/@repository.repository.originRepositoryName">@repository.repository.originUserName/@repository.repository.originRepositoryName</a></div>
}
@if(repository.repository.description.isDefined){
<div>@repository.repository.description</div>
}
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
</div>
</div>
</div>
}
}
}

View File

@@ -1,14 +0,0 @@
@(account: model.Account, active: String)(implicit context: app.Context)
@import context._
@import view.helpers._
<ul class="nav nav-tabs">
<li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
<li@if(active == "activity"){ class="active"}><a href="@url(account.userName)?tab=activity">Public Activity</a></li>
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
<li class="pull-right">
<div class="button-group">
<a href="@url(account.userName)/_edit" class="btn">Edit Your Profile</a>
</div>
</li>
}
</ul>

View File

@@ -10,6 +10,9 @@
<li@if(active=="system"){ class="active"}>
<a href="@path/admin/system">System Settings</a>
</li>
<li>
<a href="@path/console/login.jsp">H2 Console</a>
</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,121 @@
@(account: Option[model.Account], members: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(if(account.isEmpty) "New Group" else "Update Group"){
@admin.html.menu("users"){
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_newgroup} else {@path/admin/users/@account.get.userName/_editgroup}" validate="true">
<div class="row-fluid">
<div class="span7">
<fieldset>
<label for="groupName"><strong>Group name</strong></label>
<span id="error-groupName" class="error"></span>
<input type="text" name="groupName" id="groupName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
</fieldset>
<fieldset>
<label><strong>URL (Optional)</strong></label>
<span id="error-url" class="error"></span>
<input type="text" name="url" id="url" style="width: 300px;" value="@account.map(_.url)"/>
</fieldset>
<fieldset>
<label for="avatar"><strong>Image (Optional)</strong></label>
@helper.html.uploadavatar(account)
</fieldset>
</div>
<div class="span5">
<fieldset>
<label><strong>Members</strong></label>
<ul id="members" class="collaborator">
@members.map { userName =>
<li data-name="@userName">
<a href="@path/@url(userName)">@userName</a>
<a href="#" class="remove">(remove)</a>
</li>
}
</ul>
<input type="text" id="memberName" style="width: 200px; margin-bottom: 0px;"/>
<input type="button" class="btn" value="Add" id="addMember"/>
<input type="hidden" id="memberNames" name="memberNames" value="@members.mkString(",")"/>
<div>
<span class="error" id="error-memberName"></span>
</div>
</fieldset>
</div>
</div>
<fieldset class="margin">
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create Group} else {Update Group}"/>
<a href="@path/admin/users" class="btn">Cancel</a>
</fieldset>
</form>
}
}
<script>
$(function(){
$('#memberName').typeahead({
source: function (query, process) {
return $.get('@path/_user/proposals', { query: query },
function (data) {
return process(data.options);
});
}
});
$('#addMember').click(function(){
$('#error-memberName').text('');
var userName = $('#memberName').val();
// check empty
if($.trim(userName) == ''){
return false;
}
// check duplication
var exists = $('#members li').filter(function(){
return $(this).data('name') == userName;
}).length > 0;
if(exists){
$('#error-memberName').text('User has been already added.');
return false;
}
// check existence
$.post('@path/admin/users/_usercheck', {
'userName': userName
}, function(data, status){
if(data == 'true'){
// add member
$('#members').append($('<li>')
.data('name', userName)
.append($('<a>').attr('href', '@path/' + userName).text(userName))
.append(' ')
.append($('<a>').attr('href', '#').addClass('remove').text('(remove)')));
$('#memberName').val('');
// update hidden value
var userNames = $('#members li').map(function(i, e){
return $(e).data('name');
}).get().join(',');
$('#memberNames').val(userNames);
} else {
$('#error-memberName').text('User does not exist.');
}
});
});
$(document).on('click', '.remove', function(){
// remove member
$(this).parent().remove();
// update hidden value
var userNames = $('#members li').map(function(i, e){
return $(e).data('name');
}).get().join(',');
$('#memberNames').val(userNames);
});
// Don't submit form by ENTER key
$('#memberName').keypress(function(e){
console.log(e.keyCode);
return !(e.keyCode == 13);
});
});
</script>

View File

@@ -1,30 +1,46 @@
@(users: List[model.Account])(implicit context: app.Context)
@(users: List[model.Account], members: Map[String, List[String]])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Manage Users"){
@admin.html.menu("users"){
<div style="text-align: right; margin-bottom: 4px;">
<a href="@path/admin/users/_new" class="btn">New User</a>
<a href="@path/admin/users/_newuser" class="btn">New User</a>
<a href="@path/admin/users/_newgroup" class="btn">New Group</a>
</div>
<table class="table table-bordered table-hover">
@users.map { account =>
<tr>
<td>
<div class="pull-right">
<a href="@path/admin/users/@account.userName/_edit">Edit</a>
@if(account.isGroupAccount){
<a href="@path/admin/users/@account.userName/_editgroup">Edit</a>
} else {
<a href="@path/admin/users/@account.userName/_edituser">Edit</a>
}
</div>
<div class="strong">
@avatar(account.userName, 20)
<a href="@url(account.userName)">@account.userName</a>
@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)
}
}
</div>
<div>
<hr>
<i class="icon-envelope"></i> @account.mailAddress
@if(!account.isGroupAccount){
<i class="icon-envelope"></i> @account.mailAddress
}
@account.url.map { url =>
<i class="icon-home"></i> @url
}
@@ -32,7 +48,9 @@
<div>
<span class="muted">Registered:</span> @datetime(account.registeredDate)
<span class="muted">Updated:</span> @datetime(account.updatedDate)
<span class="muted">Last Login:</span> @account.lastLoginDate.map(datetime)
@if(!account.isGroupAccount){
<span class="muted">Last Login:</span> @account.lastLoginDate.map(datetime)
}
</div>
</td>
</tr>

View File

@@ -2,27 +2,28 @@
@import context._
@html.main(if(account.isEmpty) "New User" else "Update User"){
@admin.html.menu("users"){
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_new} else {@path/admin/users/@account.get.userName/_edit}" validate="true">
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_newuser} else {@path/admin/users/@account.get.userName/_edituser}" validate="true">
<div class="row-fluid">
<div class="span6">
<fieldset>
<label for="userName"><strong>Username</strong></label>
<input type="text" name="userName" id="userName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
<span id="error-userName" class="error"></span>
<input type="text" name="userName" id="userName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
</fieldset>
<fieldset>
<label for="password"><strong>Password</strong>
<label for="password">
<strong>Password</strong>
@if(account.isDefined){
(Input to change password)
}
</label>
<input type="password" name="password" id="password" value="" autocomplete="off"/>
<span id="error-password" class="error"></span>
<input type="password" name="password" id="password" value="" autocomplete="off"/>
</fieldset>
<fieldset>
<label for="mailAddress"><strong>Mail Address</strong></label>
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
<span id="error-mailAddress" class="error"></span>
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
</fieldset>
<fieldset>
<label><strong>User Type</strong></label>
@@ -35,8 +36,8 @@
</fieldset>
<fieldset>
<label><strong>URL (Optional)</strong></label>
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
<span id="error-url" class="error"></span>
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
</fieldset>
</div>
<div class="span6">

View File

@@ -12,31 +12,33 @@
@activityMessage(activity.message)
</div>
@activity.additionalInfo.map { additionalInfo =>
@(activity.activityType match {
case "create_wiki" => {
<div class="small activity-message">Created <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${additionalInfo}"}>{additionalInfo}</a>.</div>
}
case "edit_wiki" => {
<div class="small activity-message">Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${additionalInfo}"}>{additionalInfo}</a>.</div>
}
case "push" => {
<div class="small activity-message">
{additionalInfo.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) =>
if(i == 3){
<div>...</div>
} else {
<div>
<a href={s"${path}/${activity.userName}/${activity.repositoryName}/commit/${commit.substring(0, 40)}"} class="monospace">{commit.substring(0, 7)}</a>
<span>{commit.substring(41)}</span>
</div>
}
}}
</div>
}
case _ => {
<div class=" activity-message">{additionalInfo}</div>
}
})
@if(additionalInfo.nonEmpty){
@(activity.activityType match {
case "create_wiki" => {
<div class="small activity-message">Created <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${additionalInfo}"}>{additionalInfo}</a>.</div>
}
case "edit_wiki" => {
<div class="small activity-message">Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${additionalInfo}"}>{additionalInfo}</a>.</div>
}
case "push" => {
<div class="small activity-message">
{additionalInfo.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) =>
if(i == 3){
<div>...</div>
} else {
<div>
<a href={s"${path}/${activity.userName}/${activity.repositoryName}/commit/${commit.substring(0, 40)}"} class="monospace">{commit.substring(0, 7)}</a>
<span>{commit.substring(41)}</span>
</div>
}
}}
</div>
}
case _ => {
<div class=" activity-message">{additionalInfo}</div>
}
})
}
}
</div>
}

View File

@@ -1,32 +1,32 @@
@(page: Int, count: Int, limit: Int, width: Int, baseURL: String)
@defining(view.Pagination(page, count, service.IssuesService.IssueLimit, width)){ p =>
@defining(view.Pagination(page, count, limit, width)){ p =>
@if(p.count > p.limit){
<div class="pagination">
<ul>
@if(page == 1){
<li class="disabled"><span>&#9664;</span></li>
<li class="disabled"><span>&#9664;</span></li>
} else {
<li><a href="@baseURL&page=@(page - 1)">&#9664;</a></li>
<li><a href="@baseURL&page=@(page - 1)">&#9664;</a></li>
}
@for(i <- 1 to p.max){
@if(i == p.max && p.omitRight){
<li><span>&hellip;</span></li>
}
@if(i == page){
<li class="active"><span>@i</span></li>
} else {
@if(p.visibleFor(i)){
<li><a href="@baseURL&page=@i">@i</a></li>
}
}
@if(i == 1 && p.omitLeft){
<li><span>&hellip;</span></li>
}
@if(i == p.max && p.omitRight){
<li><span>&hellip;</span></li>
}
@if(i == page){
<li class="active"><span>@i</span></li>
} else {
@if(p.visibleFor(i)){
<li><a href="@baseURL&page=@i">@i</a></li>
}
}
@if(i == 1 && p.omitLeft){
<li><span>&hellip;</span></li>
}
}
@if(page == p.max){
<li class="disabled"><span>&#9654;</span></li>
<li class="disabled"><span>&#9654;</span></li>
} else {
<li><a href="@baseURL&page=@(page + 1)">&#9654;</a></li>
<li><a href="@baseURL&page=@(page + 1)">&#9654;</a></li>
}
</ul>
</div>

View File

@@ -5,7 +5,7 @@
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("New Issue - " + repository.owner + "/" + repository.name){
@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("issues", repository)
@tab("", repository)
<form action="@url(repository)/issues/new" method="POST" validate="true">
@@ -22,7 +22,6 @@
<input type="hidden" name="assignedUserName" value=""/>
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
<li class="divider"></li>
@collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-while"></i>@avatar(collaborator, 20) @collaborator</a></li>
}
@@ -34,9 +33,23 @@
<input type="hidden" name="milestoneId" value=""/>
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li>
<li class="divider"></li>
@milestones.map { milestone =>
<li><a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId"><i class="icon-while"></i> @milestone.title</a></li>
<li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
<i class="icon-while"></i> @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
}
@@ -91,7 +104,7 @@ $(function(){
});
$('a.milestone').click(function(){
var title = $(this).text();
var title = $(this).data('title');
var milestoneId = $(this).data('id');
$('a.milestone i.icon-ok').attr('class', 'icon-white');

View File

@@ -2,13 +2,13 @@
comments: List[model.IssueComment],
issueLabels: List[model.Label],
collaborators: List[String],
milestones: List[model.Milestone],
milestones: List[(model.Milestone, Int, Int)],
labels: List[model.Label],
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("%s - Issue #%d - %s/%s".format(issue.title, issue.issueId, repository.owner, repository.name)){
@html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("issues", repository)
@tab("issues", repository)
<ul class="nav nav-tabs">
@@ -38,7 +38,6 @@
@if(hasWritePermission){
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
<li class="divider"></li>
@collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li>
}
@@ -47,17 +46,38 @@
<div class="pull-right">
<span id="label-milestone">
@issue.milestoneId.map { milestoneId =>
@milestones.find(_.milestoneId == milestoneId).map { milestone =>
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
Milestone: <strong>@milestone.title</strong>
}
}.getOrElse("No milestone")
</span>
<div id="milestone-progress-area">
@issue.milestoneId.map { milestoneId =>
@milestones.collect { case (milestone, openCount, closeCount) if(milestone.milestoneId == milestoneId) =>
@issues.milestones.html.progress(openCount + closeCount, closeCount, false)
}
}
</div>
@if(hasWritePermission){
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li>
<li class="divider"></li>
@milestones.map { milestone =>
<li><a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId"><i class="icon-white"></i> @milestone.title</a></li>
@milestones.map { case (milestone, _, _) =>
<li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
<i class="icon-white"></i> @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
}
@@ -68,7 +88,14 @@
</div>
</div>
</div>
<div class="issue-participants">
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
<strong>@participants.size</strong> @plural(participants.size, "participant")
@participants.map { participant => <a href="@url(participant)">@avatar(participant, 20, tooltip = true)</a> }
}
</div>
@comments.map { comment =>
@if(comment.action != "close" && comment.action != "reopen"){
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
<div class="box issue-comment-box" id="comment-@comment.commentId">
<div class="box-header-small">
@@ -76,7 +103,7 @@
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> commented
<span class="pull-right">
@datetime(comment.registeredDate)
@if(hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false)){
@if(comment.action != "commit" && (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>
}
</span>
@@ -85,15 +112,17 @@
@markdown(comment.content, repository, false, true)
</div>
</div>
@comment.action.map { action =>
}
@if(comment.action == "close" || comment.action == "close_comment"){
<div class="small issue-comment-action">
@if(action == "close"){
<span class="label label-important">Closed</span>
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> closed the issue @datetime(comment.registeredDate)
} else {
<span class="label label-success">Reopened</span>
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> reopened the issue @datetime(comment.registeredDate)
}
<span class="label label-important">Closed</span>
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> closed the issue @datetime(comment.registeredDate)
</div>
}
@if(comment.action == "reopen" || comment.action == "reopen_comment"){
<div class="small issue-comment-action">
<span class="label label-success">Reopened</span>
<a href="@url(comment.commentedUserName)" class="username strong">@comment.commentedUserName</a> reopened the issue @datetime(comment.registeredDate)
</div>
}
}
@@ -122,7 +151,9 @@
<span class="label label-success issue-status">Open</span>
}
<div class="small" style="text-align: center;">
<strong>@comments.size</strong> @plural(comments.size, "comment")
@defining(comments.filter( _.action.contains("comment") ).size){ count =>
<strong>@count</strong> @plural(count, "comment")
}
</div>
<hr/>
<div style="margin-bottom: 8px;">
@@ -170,7 +201,8 @@ $(function(){
});
$('a.assign').click(function(){
var userName = $(this).data('name');
var $this = $(this);
var userName = $this.data('name');
$.post('@url(repository)/issues/@issue.issueId/assign',
{
assignedUserName: userName
@@ -180,27 +212,31 @@ $(function(){
if(userName == ''){
$('#label-assigned').text('No one is assigned');
} else {
$('#label-assigned').html($('<span>')
$('#label-assigned').empty()
.append($this.find('img.avatar').clone(false)).append(' ')
.append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName))
.append(' is assigned'));
.append(' is assigned');
$('a.assign[data-name=' + userName + '] i').attr('class', 'icon-ok');
}
});
});
$('a.milestone').click(function(){
var title = $(this).text();
var title = $(this).data('title');
var milestoneId = $(this).data('id');
$.post('@url(repository)/issues/@issue.issueId/milestone',
{
milestoneId: milestoneId
},
function(){
function(data){
console.log(data);
$('a.milestone i.icon-ok').attr('class', 'icon-white');
if(milestoneId == ''){
$('#label-milestone').text('No milestone');
$('#milestone-progress-area').empty();
} else {
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<strong>').text(title)));
$('#milestone-progress-area').html(data);
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
}
});

View File

@@ -15,7 +15,7 @@
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Issues - " + repository.owner + "/" + repository.name){
@html.main(s"Issues - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("issues", repository)
@tab("issues", repository)
<div class="row-fluid">
@@ -65,21 +65,45 @@
@helper.html.checkicon(condition.milestoneId == Some(None)) Issues with no milestone
</a>
</li>
@milestones.map { milestone =>
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
<li>
<a href="@condition.copy(milestoneId = Some(Some(milestone.milestoneId))).toURL">
@helper.html.checkicon(condition.milestoneId == Some(Some(milestone.milestoneId))) @milestone.title
</a>
<a href="@condition.copy(milestoneId = Some(Some(milestone.milestoneId))).toURL">
@helper.html.checkicon(condition.milestoneId == Some(Some(milestone.milestoneId))) @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
@if(condition.milestoneId.isDefined && condition.milestoneId.get.isDefined){
<div class="milestone-progress" style="margin-top: 8px;">
@if(closedCount > 0){
<span class="milestone-progress" style="width: @((closedCount.toDouble / (openCount + closedCount).toDouble * 100).toInt)%;"></span>
@milestones.find(_.milestoneId == condition.milestoneId.get.get).map { milestone =>
<div style="margin-top: 4px;">
@_root_.issues.milestones.html.progress(openCount + closedCount, closedCount, false)
</div>
<span class="muted small">@openCount open issues</span>
@if(milestone.closedDate.isDefined){
@milestone.closedDate.map { closedDate =>
<span class="small">Closed in @date(closedDate)</span>
}
} else {
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert.png"/><span class="small milestone-alert">Due in @date(dueDate)</span>
} else {
<span class="small">Due in @date(dueDate)</span>
}
}
}
</div>
<span class="muted small">@openCount open issues</span>
}
}
<hr/>
<strong>Labels</strong>
@@ -198,16 +222,29 @@
}
@helper.html.dropdown("Assignee") {
<li><a href="javascript:void(0);" class="toggle-assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
<li class="divider"></li>
@collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li>
}
}
@helper.html.dropdown("Milestone") {
<li><a href="javascript:void(0);" class="toggle-milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
<li class="divider"></li>
@milestones.map { milestone =>
<li><a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId"><i class="icon-white"></i> @milestone.title</a></li>
<li>
<a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId">
<i class="icon-white"></i> @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
</td>
@@ -227,7 +264,7 @@
}
<span class="pull-right muted">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20, true)
<a href="@url(userName)">@avatar(userName, 20, tooltip = true)</a>
}
#@issue.issueId
</span>
@@ -247,6 +284,12 @@
</div>
</div>
</div>
@if(hasWritePermission){
<form id="batcheditForm" method="POST">
<input type="hidden" name="value"/>
<input type="hidden" name="checked"/>
</form>
}
}
@if(hasWritePermission){
<script>
@@ -269,10 +312,11 @@ $(function(){
var submitBatchEdit = function(action, value) {
var checked = $('.table-issues input[type=checkbox]').filter(':checked').map(function(){ return this.value; }).get().join();
$('<form>').attr({action : action, method : 'POST'})
.append($('<input type="hidden">').attr('name', 'value').val(value))
.append($('<input type="hidden">').attr('name', 'checked').val(checked))
.submit();
var form = $('#batcheditForm');
form.find('input[name=value]').val(value);
form.find('input[name=checked]').val(checked);
form.attr('action', action);
form.submit();
};
$('#state').click(function(){

View File

@@ -1,7 +1,7 @@
@(milestone: Option[model.Milestone], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Milestones - " + repository.owner + "/" + repository.name){
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){
@html.header("milestones", repository)
@issues.html.tab("milestones", repository)
<form method="POST" action="@url(repository)/issues/milestones/@if(milestone.isEmpty){new}else{@milestone.get.milestoneId/edit}" validate="true">

View File

@@ -4,7 +4,7 @@
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Milestones - " + repository.owner + "/" + repository.name){
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){
@html.header("milestones", repository)
@issues.html.tab("milestones", repository)
<div class="row-fluid">
@@ -42,9 +42,13 @@
@if(milestone.closedDate.isDefined){
<span class="muted">Closed @datetime(milestone.closedDate.get)</span>
} else {
@if(milestone.dueDate.isDefined){
<span class="muted">Due in @date(milestone.dueDate.get)</span>
} else {
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert.png"/><span class="muted milestone-alert">Due in @date(dueDate)</span>
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
}
@@ -65,18 +69,7 @@
</div>
<span class="muted">@closedCount closed - @openCount open</span>
</div>
<div class="milestone-progress">
@if(closedCount > 0){
<span class="milestone-progress" style="width: @((closedCount.toDouble / (openCount + closedCount).toDouble * 100).toInt)%;"></span>
}
<span class="milestone-percentage">
@if(closedCount == 0){
0%
} else {
@((closedCount.toDouble / (openCount + closedCount).toDouble * 100).toInt)%
}
</span>
</div>
@progress(openCount + closedCount, closedCount, true)
</div>
</div>
@if(milestone.description.isDefined){

View File

@@ -0,0 +1,15 @@
@(total: Int, progress: Int, showPercentage: Boolean)
<div class="milestone-progress">
@if(progress > 0){
<span class="milestone-progress" style="width: @((progress.toDouble / total.toDouble * 100).toInt)%;"></span>
}
@if(showPercentage){
<span class="milestone-percentage">
@if(progress == 0){
0%
} else {
@((progress.toDouble / total.toDouble * 100).toInt)%
}
</span>
}
</div>

View File

@@ -1,4 +1,4 @@
@(title: String)(body: Html)(implicit context: app.Context)
@(title: String, repository: Option[service.RepositoryService.RepositoryInfo] = None)(body: Html)(implicit context: app.Context)
@import context._
@import view.helpers._
<!DOCTYPE html>
@@ -26,32 +26,40 @@
<script src="@assets/datepicker/js/bootstrap-datepicker.js"></script>
<script src="@assets/colorpicker/js/bootstrap-colorpicker.js"></script>
<script src="@assets/google-code-prettify/prettify.js"></script>
<script src="@assets/zclip/ZeroClipboard.min.js"></script>
</head>
<body>
<div class="navbar">
<div class="navbar-inner">
<div class="container">
<button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="brand" href="@path/">GitBucket</a>
<div class="nav-collapse collapse pull-right">
@if(loginAccount.isDefined){
<a href="@url(loginAccount.get.userName)" class="username menu">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</a>
<a href="@path/new" class="menu" data-toggle="tooltip" data-placement="bottom" title="Create a new repo"><i class="icon-plus"></i></a>
<a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user"></i></a>
@if(loginAccount.get.isAdmin){
<a href="@path/admin/users" class="menu" data-toggle="tooltip" data-placement="bottom" title="Administration"><i class="icon-wrench"></i></a>
<form id="search" action="@path/search" method="POST">
<div class="navbar">
<div class="navbar-inner">
<div class="container">
<button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="brand" href="@path/">GitBucket</a>
<div class="nav-collapse collapse pull-right header-menu">
@repository.map { repository =>
<input type="text" name="query" style="width: 300px; margin-bottom: 0px;" placeholder="Search this repository"/>
<input type="hidden" name="owner" value="@repository.owner"/>
<input type="hidden" name="repository" value="@repository.name"/>
}
<a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a>
} else {
<a href="@path/signin?@currentUrl" class="btn btn-last">Sign in</a>
}
</div><!--/.nav-collapse -->
@if(loginAccount.isDefined){
<a href="@url(loginAccount.get.userName)" class="username menu">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</a>
<a href="@path/new" class="menu" data-toggle="tooltip" data-placement="bottom" title="Create a new repo"><i class="icon-plus"></i></a>
<a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user"></i></a>
@if(loginAccount.get.isAdmin){
<a href="@path/admin/users" class="menu" data-toggle="tooltip" data-placement="bottom" title="Administration"><i class="icon-wrench"></i></a>
}
<a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a>
} else {
<a href="@path/signin?@currentUrl" class="btn btn-last">Sign in</a>
}
</div><!--/.nav-collapse -->
</div>
</div>
</div>
</form>
@defining(servlet.AutoUpdate.getCurrentVersion){ version =>
<div class="gitbucket-version">version @version.majorVersion.@version.minorVersion</div>
}
@@ -59,5 +67,12 @@
<div class="container body">
@body
</div>
<script>
$(function(){
$('#search').submit(function(){
return $.trim($(this).find('input[name=query]').val()) != '';
});
});
</script>
</body>
</html>
</html>

View File

@@ -1,47 +1,73 @@
@()(implicit context: app.Context)
@(groupNames: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@main("Create a New Repository"){
<div style="width: 600px; margin: 10px auto;">
<form id="form" method="post" action="@path/new" validate="true">
<fieldset>
<label for="name"><strong>Repository name</strong></label>
<input type="text" name="name" id="name" />
<span id="error-name" class="error"></span>
</fieldset>
<fieldset>
<label for="description"><strong>Description</strong> (optional)</label>
<input type="text" name="description" id="description" style="width: 95%;"/>
</fieldset>
<fieldset class="margin">
<label>
<input type="radio" name="isPrivate" value="false" checked>
<strong>Public</strong><br>
<div>
<span class="note">All users and guests can read this repository.</span>
<form id="form" method="post" action="@path/new" validate="true">
<fieldset>
<label for="name"><strong>Repository name</strong></label>
<div class="btn-group" style="margin-bottom: 10px;" id="owner-dropdown">
<button class="btn dropdown-toggle" data-toggle="dropdown">
<strong>@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</strong>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a href="javascript:void(0);" data-name="@loginAccount.get.userName"><i class="icon-ok"></i> <span>@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</span></a></li>
@groupNames.map { groupName =>
<li><a href="javascript:void(0);" data-name="@groupName"><i class="icon-white"></i> <span>@avatar(groupName, 20) @groupName</span></a></li>
}
</ul>
<input type="hidden" name="owner" id="owner" />
</div>
</label>
</fieldset>
<fieldset>
<label>
<input type="radio" name="isPrivate" value="true">
<strong>Private</strong><br>
<div>
<span class="note">Only collaborators can read this repository.</span>
</div>
</label>
</fieldset>
<fieldset class="margin">
<label for="createReadme">
<input type="checkbox" name="createReadme" id="createReadme"/>
<strong>Initialize this repository with a README</strong>
<div>
<span class="note">This will allow you to <code>git clone</code> the repository immediately.</span>
</div>
</label>
</fieldset>
<fieldset class="margin">
<input type="submit" class="btn btn-success" value="Create repository"/>
</fieldset>
</form>
/
<input type="text" name="name" id="name" />
<span id="error-name" class="error"></span>
</fieldset>
<fieldset>
<label for="description"><strong>Description</strong> (optional)</label>
<input type="text" name="description" id="description" style="width: 95%;"/>
</fieldset>
<fieldset class="margin">
<label>
<input type="radio" name="isPrivate" value="false" checked>
<strong>Public</strong><br>
<div>
<span class="note">All users and guests can read this repository.</span>
</div>
</label>
</fieldset>
<fieldset>
<label>
<input type="radio" name="isPrivate" value="true">
<strong>Private</strong><br>
<div>
<span class="note">Only collaborators can read this repository.</span>
</div>
</label>
</fieldset>
<fieldset class="margin">
<label for="createReadme">
<input type="checkbox" name="createReadme" id="createReadme"/>
<strong>Initialize this repository with a README</strong>
<div>
<span class="note">This will allow you to <code>git clone</code> the repository immediately.</span>
</div>
</label>
</fieldset>
<fieldset class="margin">
<input type="submit" class="btn btn-success" value="Create repository"/>
</fieldset>
</form>
</div>
}
<script>
$('#owner-dropdown a').click(function(){
var userName = $(this).data('name');
$('#owner').val(userName);
$('#owner-dropdown i').attr('class', 'icon-white');
$(this).find('i').attr('class', 'icon-ok');
$('#owner-dropdown strong').html($(this).find('span').html());
});
</script>

View File

@@ -5,7 +5,7 @@
latestCommit: util.JGitUtil.CommitInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(repository.owner+"/"+repository.name) {
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.header("code", repository)
@tab(branch, repository, "files")
<div class="head">
@@ -23,7 +23,7 @@
<tr>
<th style="font-weight: normal;">
<div class="pull-left">
@avatar(latestCommit.committer, 20)
@avatar(latestCommit, 20)
<a href="@url(latestCommit.committer)" class="username strong">@latestCommit.committer</a>
<span class="muted">@datetime(latestCommit.time)</span>
<a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a>

View File

@@ -8,7 +8,7 @@
@import view.helpers._
@import util.Implicits._
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
@html.main(commit.shortMessage){
@html.main(commit.shortMessage, Some(repository)){
@html.header("code", repository)
@tab(commitId, repository, "commits")
<table class="table table-bordered">
@@ -43,7 +43,7 @@
</tr>
<tr>
<td>
@avatar(commit.committer, 20)
@avatar(commit, 20)
<a href="@url(commit.committer)" class="username strong">@commit.committer</a>
<span class="muted">@datetime(commit.time)</span>
<div class="pull-right monospace small" style="text-align: right;">

View File

@@ -6,7 +6,7 @@
hasNext: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(repository.owner+"/"+repository.name) {
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.header("code", repository)
@tab(branch, repository, if(pathList.isEmpty) "commits" else "files")
<div class="head">
@@ -38,7 +38,7 @@
<a href="@url(repository)/tree/@commit.id" class="small">Browse code</a>
</div>
<div>
<div class="commit-avatar-image">@avatar(commit.committer, 40)</div>
<div class="commit-avatar-image">@avatar(commit, 40)</div>
<div class="commit-message-box">
<a href="@url(repository)/commit/@commit.id" class="commit-message" style="font-weight: bold;">@link(commit.summary, repository)</a>
@if(commit.description.isDefined){

View File

@@ -6,7 +6,7 @@
readme: Option[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(repository.owner + "/" + repository.name) {
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.header("code", repository)
@tab(branch, repository, "files")
<div class="head">
@@ -34,7 +34,7 @@
<tr>
<td colspan="4" class="latest-commit">
<div>
@avatar(latestCommit.committer, 20)
@avatar(latestCommit, 20)
<a href="@url(latestCommit.committer)" class="username strong">@latestCommit.committer</a>
<span class="muted">@datetime(latestCommit.time)</span>
<div class="pull-right align-right monospace">

View File

@@ -1,7 +1,7 @@
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(repository.owner + "/" + repository.name) {
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.header("code", repository)
<h3 style="margin-top: 30px;">Create a new repository on the command line</h3>
<pre>

View File

@@ -30,9 +30,9 @@
<li@if(active=="commits"){ class="active"}><a href="@url(repository)/commits/@id">Commits</a></li>
<li@if(active=="tags"){ class="active"}><a href="@url(repository)/tags">Tags@if(repository.tags.length > 0){ <span class="badge">@repository.tags.length</span>}</a></li>
<li class="pull-right">
<div class="input-prepend">
<span class="add-on">HTTP</span>
<div class="input-append">
<input type="text" value="@repository.url" id="repository-url" readonly>
<span id="repository-url-copy" class="add-on btn" data-clipboard-text="@repository.url" data-placement="bottom" title="copy to clipboard"><i class="icon-check"></i></span>
</div>
</li>
</ul>

View File

@@ -1,7 +1,7 @@
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(repository.owner + "/" + repository.name) {
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.header("code", repository)
@tab(repository.repository.defaultBranch, repository, "tags", true)
<h1>Tags</h1>

View File

@@ -0,0 +1,26 @@
@(files: List[service.RepositorySearchService.FileSearchResult],
issueCount: Int,
query: String,
page: Int,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@import service.RepositorySearchService._
@html.main("Search Results", Some(repository)){
@menu("code", files.size, issueCount, query, repository){
@if(files.isEmpty){
<h4>We couldn't find any code matching '@query'</h4>
} else {
<h4>We've found @files.size code @plural(files.size, "result")</h4>
}
@files.drop((page - 1) * CodeLimit).take(CodeLimit).map { file =>
<div>
<h5><a href="@url(repository)/blob/@repository.repository.defaultBranch/@file.path">@file.path</a></h5>
<div class="small muted">Latest commit at @datetime(file.lastModified)</div>
<pre class="prettyprint linenums:@file.highlightLineNumber" style="padding-left: 25px;">@Html(file.highlightText)</pre>
</div>
}
@helper.html.paginator(page, files.size, CodeLimit, 10,
s"${url(repository)}/search?q=${urlEncode(query)}&type=code")
}
}

View File

@@ -0,0 +1,35 @@
@(issues: List[service.RepositorySearchService.IssueSearchResult],
fileCount: Int,
query: String,
page: Int,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@import service.RepositorySearchService._
@html.main("Search Results", Some(repository)){
@menu("issue", fileCount, issues.size, query, repository){
@if(issues.isEmpty){
<h4>We couldn't find any code matching '@query'</h4>
} else {
<h4>We've found @issues.size code @plural(issues.size, "result")</h4>
}
@issues.drop((page - 1) * IssueLimit).take(IssueLimit).map { issue =>
<div class="block">
<div class="pull-right muted">#@issue.issueId</div>
<h4 style="margin-top: 0px;"><a href="@url(repository)/issues/@issue.issueId">@issue.title</a></h4>
@if(issue.highlightText.nonEmpty){
<pre>@Html(issue.highlightText)</pre>
}
<div class="small muted">
Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a>
at @datetime(issue.registeredDate)
@if(issue.commentCount > 0){
&nbsp;&nbsp;<i class="icon-comment"></i><strong>@issue.commentCount</strong> @plural(issue.commentCount, "comment")
}
</div>
</div>
}
@helper.html.paginator(page, issues.size, IssueLimit, 10,
s"${url(repository)}/search?q=${urlEncode(query)}&type=issue")
}
}

View File

@@ -0,0 +1,37 @@
@(active: String, fileCount: Int, issueCount: Int, query: String,
repository: service.RepositoryService.RepositoryInfo)(body: Html)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.header("", repository)
<div class="row-fluid">
<div class="span3">
<div class="box">
<ul class="nav nav-tabs nav-stacked side-menu">
<li@if(active=="code"){ class="active"}>
<a href="@url(repository)/search?q=@urlEncode(query)&type=code">
@if(fileCount != 0){
<span class="badge pull-right">@fileCount</span>
}
Code
</a>
</li>
<li@if(active=="issue"){ class="active"}>
<a href="@url(repository)/search?q=@urlEncode(query)&type=issue">
@if(issueCount != 0){
<span class="badge pull-right">@issueCount</span>
}
Issue
</a>
</li>
</ul>
</div>
</div>
<div class="span9">
<form action="@url(repository)/search" method="GET">
<input type="text" name="q" value="@query" style="width: 80%; margin-bottom: 0px;"/>
<input type="submit" value="Search" class="btn" style="width: 15%;"/>
<input type="hidden" name="type" value="@active"/>
</form>
@body
</div>
</div>

View File

@@ -1,7 +1,9 @@
@(collaborators: List[String], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@(collaborators: List[String],
isGroupRepository: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Settings"){
@html.main("Settings", Some(repository)){
@html.header("settings", repository)
@menu("collaborators", repository){
<h3>Manage Collaborators</h3>
@@ -9,26 +11,32 @@
@collaborators.map { collaboratorName =>
<li>
<a href="@url(collaboratorName)">@collaboratorName</a>
<a href="@url(repository)/settings/collaborators/remove?name=@collaboratorName" class="remove">(remove)</a>
@if(!isGroupRepository){
<a href="@url(repository)/settings/collaborators/remove?name=@collaboratorName" class="remove">(remove)</a>
}
</li>
}
</ul>
<form method="POST" action="@url(repository)/settings/collaborators/add" validate="true">
<div>
<span class="error" id="error-userName"></span>
</div>
<input type="text" name="userName" id="userName" style="width: 300px; margin-bottom: 0px;"/>
<input type="submit" class="btn" value="Add"/>
</form>
@if(!isGroupRepository){
<form method="POST" action="@url(repository)/settings/collaborators/add" validate="true">
<div>
<span class="error" id="error-userName"></span>
</div>
<input type="text" name="userName" id="userName" style="width: 300px; margin-bottom: 0px;"/>
<input type="submit" class="btn" value="Add"/>
</form>
}
}
}
<script>
$('#userName').typeahead({
source: function (query, process) {
return $.get('@url(repository)/settings/collaborators/proposals', { query: query },
function (data) {
return process(data.options);
});
}
$(function(){
$('#userName').typeahead({
source: function (query, process) {
return $.get('@path/_user/proposals', { query: query },
function (data) {
return process(data.options);
});
}
});
});
</script>

View File

@@ -1,7 +1,7 @@
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Delete Repository"){
@html.main("Delete Repository", Some(repository)){
@html.header("settings", repository)
@menu("delete", repository){
<form id="form" method="post" action="@url(repository)/settings/delete">

View File

@@ -1,7 +1,7 @@
@(repository: service.RepositoryService.RepositoryInfo, info: Option[Any])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Settings"){
@html.main("Settings", Some(repository)){
@html.header("settings", repository)
@menu("options", repository){
@helper.html.information(info)
@@ -23,24 +23,24 @@
</select>
</fieldset>
<hr>
<fieldset class="margin">
<label>
<input type="radio" name="isPrivate" value="false" checked>
<strong>Public</strong><br>
<div>
<span class="note">All users and guests can read this repository.</span>
</div>
</label>
</fieldset>
<fieldset>
<label>
<input type="radio" name="isPrivate" value="true">
<strong>Private</strong><br>
<div>
<span class="note">Only collaborators can read this repository.</span>
</div>
</label>
</fieldset>
<fieldset class="margin">
<label>
<input type="radio" name="isPrivate" value="false"@if(!repository.repository.isPrivate){ checked}>
<strong>Public</strong><br>
<div>
<span class="note">All users and guests can read this repository.</span>
</div>
</label>
</fieldset>
<fieldset>
<label>
<input type="radio" name="isPrivate" value="true"@if(repository.repository.isPrivate){ checked}>
<strong>Private</strong><br>
<div>
<span class="note">Only collaborators can read this repository.</span>
</div>
</label>
</fieldset>
</div>
</div>
@*

View File

@@ -4,7 +4,7 @@
@import context._
@import view.helpers._
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
@html.main("Compare Revisions - " + repository.owner + "/" + repository.name){
@html.main(s"Compare Revisions - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("wiki", repository)
@tab("history", repository)
<ul class="nav nav-tabs">

View File

@@ -3,7 +3,7 @@
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main((if(pageName == "") "New Page" else pageName) + " - " + repository.owner + "/" + repository.name){
@html.main(s"${if(pageName == "") "New Page" else pageName} - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("wiki", repository)
@tab("", repository)
<ul class="nav nav-tabs">

View File

@@ -3,7 +3,7 @@
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("History - " + repository.owner + "/" + repository.name){
@html.main(s"History - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("wiki", repository)
@tab(if(pageName.isEmpty) "history" else "", repository)
<ul class="nav nav-tabs">

View File

@@ -4,7 +4,7 @@
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(pageName + " - " + repository.owner + "/" + repository.name){
@html.main(s"${pageName} - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("wiki", repository)
@tab((if(pageName == "Home") "home" else ""), repository)
<ul class="nav nav-tabs">

View File

@@ -1,7 +1,7 @@
@(pages: List[String], repository: service.RepositoryService.RepositoryInfo, hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Pages - " + repository.owner + "/" + repository.name){
@html.main(s"Pages - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("wiki", repository)
@tab("pages", repository)
<ul class="nav nav-tabs">

View File

@@ -6,9 +6,9 @@
<li@if(active == "pages"){ class="active"}><a href="@url(repository)/wiki/_pages">Pages</a></li>
<li@if(active == "history"){ class="active"}><a href="@url(repository)/wiki/_history">Wiki History</a></li>
<li class="pull-right">
<div class="input-prepend">
<span class="add-on">HTTP</span>
<div class="input-append">
<input type="text" value="@repository.url.replaceFirst("\\.git$", ".wiki.git")" readonly id="repository-url">
<span id="repository-url-copy" class="add-on btn" data-clipboard-text="@repository.url.replaceFirst("\\.git$", ".wiki.git")" data-placement="bottom" title="copy to clipboard"><i class="icon-check"></i></span>
</div>
</li>
</ul>

View File

@@ -5,6 +5,9 @@ body {
color: #333;
}
/* ======================================================================== */
/* Global Header */
/* ======================================================================== */
div.navbar-inner {
border-radius: 0px;
-webkit-border-radius: 0px;
@@ -16,18 +19,23 @@ div.navbar-inner {
padding-right: 0px;
}
div.header-menu {
line-height: 40px;
}
div.header-menu input,
div.header-menu a.btn {
margin-top: 0px;
margin-bottom: 0px;
}
div.nav-collapse a.menu {
margin-right: 12px;
line-height: 40px;
}
div.nav-collapse a.btn-last {
margin-right: 30px;
}
div.nav-collapse a.btn-last,
div.nav-collapse a.menu-last {
margin-right: 30px;
line-height: 40px;
}
div.gitbucket-version {
@@ -37,6 +45,9 @@ div.gitbucket-version {
margin-right: 10px;
}
/* ======================================================================== */
/* Repository Header */
/* ======================================================================== */
table.global-nav {
width: 920px;
margin-bottom: 10px;
@@ -59,6 +70,9 @@ table.global-nav th a:link, table.global-nav th a:hover, table.global-nav th a:v
text-decoration: none;
}
/* ======================================================================== */
/* General Styles */
/* ======================================================================== */
div.head {
font-size: large;
margin-bottom: 10px;
@@ -253,6 +267,15 @@ div.account-image {
margin-bottom: 8px;
}
ul.dropdown-menu li {
border-bottom: 1px solid #eee;
}
ul.dropdown-menu :last-child {
border-bottom: none;
}
/****************************************************************************/
/* Sign-in form */
/****************************************************************************/
@@ -437,10 +460,16 @@ a#clear-filter {
margin-bottom: 10px;
}
span.milestone-alert {
font-weight: bold;
color: #bd2c00;
}
a.milestone-title {
font-size: 120%;
font-weight: bold;
}
div.milestone-description {
border-top: 1px solid #eee;
color: #666;
@@ -459,6 +488,15 @@ div.milestone-menu a.delete {
color: #b00;
}
div#milestone-progress-area {
display: inline-block;
}
div#milestone-progress-area div.milestone-progress {
width: 150px;
margin-bottom: -6px;
}
div.milestone-progress {
position: relative;
height: 20px;
@@ -521,6 +559,11 @@ div.issue-avatar-image {
}
div.issue-box {
margin-bottom: 5px;
margin-left: 50px;
}
div.issue-participants {
margin-bottom: 15px;
margin-left: 50px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

View File

@@ -1,4 +1,5 @@
$(function(){
// disable Ajax cache
$.ajaxSetup({ cache: false });
$('#repository-url').click(function(){
@@ -8,5 +9,36 @@ $(function(){
$('img[data-toggle=tooltip]').tooltip();
$('a[data-toggle=tooltip]').tooltip();
// copy to clipboard
(function() {
// Find ZeroClipboard.swf file URI from ZeroClipboard JavaScript file path.
// NOTE(tanacasino) I think this way is wrong... but i don't know correct way.
var moviePath = (function() {
var zclipjs = "ZeroClipboard.min.js";
var scripts = document.getElementsByTagName("script");
var i = scripts.length;
while(i--) {
var match = scripts[i].src.match(zclipjs + "$");
if(match) {
return match.input.substr(0, match.input.length - 6) + 'swf';
}
}
})();
var clip = new ZeroClipboard($("#repository-url-copy"), {
moviePath: moviePath
});
var title = $('#repository-url-copy').attr('title');
$('#repository-url-copy').removeAttr('title')
clip.on('complete', function(client, args) {
$(clip.htmlBridge).attr('title', 'copied!').tooltip('fixTitle').tooltip('show');
$(clip.htmlBridge).attr('title', title).tooltip('fixTitle');
});
$(clip.htmlBridge).tooltip({
title: title,
placement: $('#repository-url-copy').attr('data-placement')
});
})();
// syntax highlighting by google-code-prettify
prettyPrint();
});

File diff suppressed because one or more lines are too long

Binary file not shown.