diff --git a/README.md b/README.md
index 4a44fce05..a5f64065b 100644
--- a/README.md
+++ b/README.md
@@ -80,6 +80,18 @@ Run the following commands in `Terminal` to
Release Notes
--------
+### 2.6 - COMING SOON!
+- Search box at issues and pull requests
+- Information from administrator
+- Some bug fix and improvements
+
+### 2.5 - 4 Nov 2014
+- New Dashboard
+- Change datetime format
+- Create branch from Web UI
+- Task list in Markdown
+- Some bug fix and improvements
+
### 2.4.1 - 6 Oct 2014
- Bug fix
diff --git a/etc/icons.svg b/etc/icons.svg
index 1d50b97e7..9372a97d2 100644
--- a/etc/icons.svg
+++ b/etc/icons.svg
@@ -1,1844 +1,754 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- image/svg+xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/scala/app/DashboardController.scala b/src/main/scala/app/DashboardController.scala
index b470f0ec9..7c445294b 100644
--- a/src/main/scala/app/DashboardController.scala
+++ b/src/main/scala/app/DashboardController.scala
@@ -1,18 +1,33 @@
package app
import service._
-import util.{UsersAuthenticator, Keys}
+import util.{StringUtil, UsersAuthenticator, Keys}
import util.Implicits._
+import service.IssuesService.IssueSearchCondition
class DashboardController extends DashboardControllerBase
with IssuesService with PullRequestService with RepositoryService with AccountService
with UsersAuthenticator
trait DashboardControllerBase extends ControllerBase {
- self: IssuesService with PullRequestService with RepositoryService with UsersAuthenticator =>
+ self: IssuesService with PullRequestService with RepositoryService with AccountService
+ with UsersAuthenticator =>
- get("/dashboard/issues/repos")(usersOnly {
- searchIssues("all")
+ get("/dashboard/issues")(usersOnly {
+ val q = request.getParameter("q")
+ val account = context.loginAccount.get
+ Option(q).map { q =>
+ val condition = IssueSearchCondition(q, Map[String, Int]())
+ q match {
+ case q if(q.contains("is:pr")) => redirect(s"/dashboard/pulls?q=${StringUtil.urlEncode(q)}")
+ case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/issues/created_by${condition.toURL}")
+ case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/issues/assigned${condition.toURL}")
+ case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/issues/mentioned${condition.toURL}")
+ case _ => searchIssues("created_by")
+ }
+ } getOrElse {
+ searchIssues("created_by")
+ }
})
get("/dashboard/issues/assigned")(usersOnly {
@@ -23,87 +38,99 @@ trait DashboardControllerBase extends ControllerBase {
searchIssues("created_by")
})
+ get("/dashboard/issues/mentioned")(usersOnly {
+ searchIssues("mentioned")
+ })
+
get("/dashboard/pulls")(usersOnly {
- searchPullRequests("created_by", None)
+ val q = request.getParameter("q")
+ val account = context.loginAccount.get
+ Option(q).map { q =>
+ val condition = IssueSearchCondition(q, Map[String, Int]())
+ q match {
+ case q if(q.contains("is:issue")) => redirect(s"/dashboard/issues?q=${StringUtil.urlEncode(q)}")
+ case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/pulls/created_by${condition.toURL}")
+ case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/pulls/assigned${condition.toURL}")
+ case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/pulls/mentioned${condition.toURL}")
+ case _ => searchPullRequests("created_by")
+ }
+ } getOrElse {
+ searchPullRequests("created_by")
+ }
})
- get("/dashboard/pulls/owned")(usersOnly {
- searchPullRequests("created_by", None)
+ get("/dashboard/pulls/created_by")(usersOnly {
+ searchPullRequests("created_by")
})
- get("/dashboard/pulls/public")(usersOnly {
- searchPullRequests("not_created_by", None)
+ get("/dashboard/pulls/assigned")(usersOnly {
+ searchPullRequests("assigned")
})
- get("/dashboard/pulls/for/:owner/:repository")(usersOnly {
- searchPullRequests("all", Some(params("owner") + "/" + params("repository")))
+ get("/dashboard/pulls/mentioned")(usersOnly {
+ searchPullRequests("mentioned")
})
+ private def getOrCreateCondition(key: String, filter: String, userName: String) = {
+ val condition = session.putAndGet(key, if(request.hasQueryString){
+ val q = request.getParameter("q")
+ if(q == null){
+ IssueSearchCondition(request)
+ } else {
+ IssueSearchCondition(q, Map[String, Int]())
+ }
+ } else session.getAs[IssueSearchCondition](key).getOrElse(IssueSearchCondition()))
+
+ filter match {
+ case "assigned" => condition.copy(assigned = Some(userName), author = None , mentioned = None)
+ case "mentioned" => condition.copy(assigned = None , author = None , mentioned = Some(userName))
+ case _ => condition.copy(assigned = None , author = Some(userName), mentioned = None)
+ }
+ }
+
private def searchIssues(filter: String) = {
import IssuesService._
- // condition
- val condition = session.putAndGet(Keys.Session.DashboardIssues,
- if(request.hasQueryString) IssueSearchCondition(request)
- else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition())
- )
-
- val userName = context.loginAccount.get.userName
- val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
- val filterUser = Map(filter -> userName)
- val page = IssueSearchCondition.page(request)
+ val userName = context.loginAccount.get.userName
+ val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName)
+ val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
+ val page = IssueSearchCondition.page(request)
dashboard.html.issues(
- dashboard.html.issueslist(
- searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
- page,
- countIssue(condition.copy(state = "open" ), filterUser, false, userRepos: _*),
- countIssue(condition.copy(state = "closed"), filterUser, false, userRepos: _*),
- condition),
- countIssue(condition.copy(assigned = None, author = None), filterUser, false, userRepos: _*),
- countIssue(condition.copy(assigned = Some(userName), author = None), filterUser, false, userRepos: _*),
- countIssue(condition.copy(assigned = None, author = Some(userName)), filterUser, false, userRepos: _*),
- countIssueGroupByRepository(condition, filterUser, false, userRepos: _*),
- condition,
- filter)
-
+ searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
+ page,
+ countIssue(condition.copy(state = "open" ), false, userRepos: _*),
+ countIssue(condition.copy(state = "closed"), false, userRepos: _*),
+ filter match {
+ case "assigned" => condition.copy(assigned = Some(userName))
+ case "mentioned" => condition.copy(mentioned = Some(userName))
+ case _ => condition.copy(author = Some(userName))
+ },
+ filter,
+ getGroupNames(userName))
}
- private def searchPullRequests(filter: String, repository: Option[String]) = {
+ private def searchPullRequests(filter: String) = {
import IssuesService._
import PullRequestService._
- // condition
- val condition = session.putAndGet(Keys.Session.DashboardPulls, {
- if(request.hasQueryString) IssueSearchCondition(request)
- else session.getAs[IssueSearchCondition](Keys.Session.DashboardPulls).getOrElse(IssueSearchCondition())
- }.copy(repo = repository))
-
- val userName = context.loginAccount.get.userName
- val allRepos = getAllRepositories(userName)
- val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
- val filterUser = Map(filter -> userName)
- val page = IssueSearchCondition.page(request)
-
- val counts = countIssueGroupByRepository(
- IssueSearchCondition().copy(state = condition.state), filterUser, true, userRepos: _*)
+ val userName = context.loginAccount.get.userName
+ val condition = getOrCreateCondition(Keys.Session.DashboardPulls, filter, userName)
+ val allRepos = getAllRepositories(userName)
+ val page = IssueSearchCondition.page(request)
dashboard.html.pulls(
- dashboard.html.pullslist(
- searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
- page,
- countIssue(condition.copy(state = "open" ), filterUser, true, allRepos: _*),
- countIssue(condition.copy(state = "closed"), filterUser, true, allRepos: _*),
- condition,
- None,
- false),
- getAllPullRequestCountGroupByUser(condition.state == "closed", userName),
- userRepos.map { case (userName, repoName) =>
- (userName, repoName, counts.find { x => x._1 == userName && x._2 == repoName }.map(_._3).getOrElse(0))
- }.sortBy(_._3).reverse,
- condition,
- filter)
-
+ searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
+ page,
+ countIssue(condition.copy(state = "open" ), true, allRepos: _*),
+ countIssue(condition.copy(state = "closed"), true, allRepos: _*),
+ filter match {
+ case "assigned" => condition.copy(assigned = Some(userName))
+ case "mentioned" => condition.copy(mentioned = Some(userName))
+ case _ => condition.copy(author = Some(userName))
+ },
+ filter,
+ getGroupNames(userName))
}
diff --git a/src/main/scala/app/IssuesController.scala b/src/main/scala/app/IssuesController.scala
index b6b7bb94a..9f6efcf3d 100644
--- a/src/main/scala/app/IssuesController.scala
+++ b/src/main/scala/app/IssuesController.scala
@@ -9,7 +9,6 @@ import util.Implicits._
import util.ControlUtil._
import org.scalatra.Ok
import model.Issue
-import plugin.PluginSystem
class IssuesController extends IssuesControllerBase
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
@@ -50,7 +49,12 @@ trait IssuesControllerBase extends ControllerBase {
)(IssueStateForm.apply)
get("/:owner/:repository/issues")(referrersOnly { repository =>
- searchIssues(repository)
+ val q = request.getParameter("q")
+ if(Option(q).exists(_.contains("is:pr"))){
+ redirect(s"/${repository.owner}/${repository.name}/pulls?q=" + StringUtil.urlEncode(q))
+ } else {
+ searchIssues(repository)
+ }
})
get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
@@ -195,7 +199,7 @@ trait IssuesControllerBase extends ControllerBase {
org.json4s.jackson.Serialization.write(
Map("title" -> x.title,
"content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
- repository, false, true)
+ repository, false, true, true, isEditable(x.userName, x.repositoryName, x.openedUserName))
))
}
} else Unauthorized
@@ -212,7 +216,7 @@ trait IssuesControllerBase extends ControllerBase {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("content" -> view.Markdown.toHtml(x.content,
- repository, false, true)
+ repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName))
))
}
} else Unauthorized
@@ -390,19 +394,25 @@ trait IssuesControllerBase extends ControllerBase {
// retrieve search condition
val condition = session.putAndGet(sessionKey,
- if(request.hasQueryString) IssueSearchCondition(request)
- else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
+ if(request.hasQueryString){
+ val q = request.getParameter("q")
+ if(q == null){
+ IssueSearchCondition(request)
+ } else {
+ IssueSearchCondition(q, getMilestones(owner, repoName).map(x => (x.title, x.milestoneId)).toMap)
+ }
+ } else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
issues.html.list(
"issues",
- searchIssue(condition, Map.empty, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
+ searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page,
(getCollaborators(owner, repoName) :+ owner).sorted,
getMilestones(owner, repoName),
getLabels(owner, repoName),
- countIssue(condition.copy(state = "open" ), Map.empty, false, owner -> repoName),
- countIssue(condition.copy(state = "closed"), Map.empty, false, owner -> repoName),
+ countIssue(condition.copy(state = "open" ), false, owner -> repoName),
+ countIssue(condition.copy(state = "closed"), false, owner -> repoName),
condition,
repository,
hasWritePermission(owner, repoName, context.loginAccount))
diff --git a/src/main/scala/app/PullRequestsController.scala b/src/main/scala/app/PullRequestsController.scala
index c079f0198..ff872415f 100644
--- a/src/main/scala/app/PullRequestsController.scala
+++ b/src/main/scala/app/PullRequestsController.scala
@@ -1,6 +1,6 @@
package app
-import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier, Keys}
+import util._
import util.Directory._
import util.Implicits._
import util.ControlUtil._
@@ -18,6 +18,9 @@ import org.slf4j.LoggerFactory
import org.eclipse.jgit.merge.MergeStrategy
import org.eclipse.jgit.errors.NoMergeBaseException
import service.WebHookService.WebHookPayload
+import util.JGitUtil.DiffInfo
+import scala.Some
+import util.JGitUtil.CommitInfo
class PullRequestsController extends PullRequestsControllerBase
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
@@ -59,7 +62,12 @@ trait PullRequestsControllerBase extends ControllerBase {
case class MergeForm(message: String)
get("/:owner/:repository/pulls")(referrersOnly { repository =>
- searchPullRequests(None, repository)
+ val q = request.getParameter("q")
+ if(Option(q).exists(_.contains("is:issue"))){
+ redirect(s"/${repository.owner}/${repository.name}/issues?q=" + StringUtil.urlEncode(q))
+ } else {
+ searchPullRequests(None, repository)
+ }
})
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
@@ -460,13 +468,13 @@ trait PullRequestsControllerBase extends ControllerBase {
issues.html.list(
"pulls",
- searchIssue(condition, Map.empty, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
+ searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
page,
(getCollaborators(owner, repoName) :+ owner).sorted,
getMilestones(owner, repoName),
getLabels(owner, repoName),
- countIssue(condition.copy(state = "open" ), Map.empty, true, owner -> repoName),
- countIssue(condition.copy(state = "closed"), Map.empty, true, owner -> repoName),
+ countIssue(condition.copy(state = "open" ), true, owner -> repoName),
+ countIssue(condition.copy(state = "closed"), true, owner -> repoName),
condition,
repository,
hasWritePermission(owner, repoName, context.loginAccount))
diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala
index 42917306d..5b5b11b1b 100644
--- a/src/main/scala/app/RepositoryViewerController.scala
+++ b/src/main/scala/app/RepositoryViewerController.scala
@@ -77,7 +77,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
contentType = "text/html"
view.helpers.markdown(params("content"), repository,
params("enableWikiLink").toBoolean,
- params("enableRefsLink").toBoolean)
+ params("enableRefsLink").toBoolean,
+ params("enableTaskList").toBoolean,
+ hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
/**
@@ -112,7 +114,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
- }, page, hasNext)
+ }, page, hasNext, hasWritePermission(repository.owner, repository.name, context.loginAccount))
case Left(_) => NotFound
}
}
@@ -239,6 +241,24 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
})
+ /**
+ * Creates a branch.
+ */
+ post("/:owner/:repository/branches")(collaboratorsOnly { repository =>
+ val newBranchName = params.getOrElse("new", halt(400))
+ val fromBranchName = params.getOrElse("from", halt(400))
+ using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
+ JGitUtil.createBranch(git, fromBranchName, newBranchName)
+ } match {
+ case Right(message) =>
+ flash += "info" -> message
+ redirect(s"/${repository.owner}/${repository.name}/tree/${StringUtil.urlEncode(newBranchName).replace("%2F", "/")}")
+ case Left(message) =>
+ flash += "error" -> message
+ redirect(s"/${repository.owner}/${repository.name}/tree/${fromBranchName}")
+ }
+ })
+
/**
* Deletes branch.
*/
@@ -331,7 +351,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repo.html.files(revision, repository,
if(path == ".") Nil else path.split("/").toList, // current path
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
- files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount))
+ files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount),
+ flash.get("info"), flash.get("error"))
}
} getOrElse NotFound
}
diff --git a/src/main/scala/app/SystemSettingsController.scala b/src/main/scala/app/SystemSettingsController.scala
index 27b18fa1b..57e4eb3d3 100644
--- a/src/main/scala/app/SystemSettingsController.scala
+++ b/src/main/scala/app/SystemSettingsController.scala
@@ -21,6 +21,7 @@ trait SystemSettingsControllerBase extends ControllerBase {
private val form = mapping(
"baseUrl" -> trim(label("Base URL", optional(text()))),
+ "information" -> trim(label("Information", optional(text()))),
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
"gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())),
diff --git a/src/main/scala/app/UserManagementController.scala b/src/main/scala/app/UserManagementController.scala
index 612503959..d98d70366 100644
--- a/src/main/scala/app/UserManagementController.scala
+++ b/src/main/scala/app/UserManagementController.scala
@@ -49,7 +49,7 @@ 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())),
- "removed" -> trim(label("Disable" ,boolean()))
+ "removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName"))))
)(EditUserForm.apply)
val newGroupForm = mapping(
@@ -190,4 +190,14 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
}
}
+ protected def disableByNotYourself(paramName: String): Constraint = new Constraint() {
+ override def validate(name: String, value: String, messages: Messages): Option[String] = {
+ params.get(paramName).flatMap { userName =>
+ if(userName == context.loginAccount.get.userName)
+ Some("You can't disable your account yourself")
+ else
+ None
+ }
+ }
+ }
}
diff --git a/src/main/scala/service/AccountService.scala b/src/main/scala/service/AccountService.scala
index b815c5411..c502eb7b4 100644
--- a/src/main/scala/service/AccountService.scala
+++ b/src/main/scala/service/AccountService.scala
@@ -168,6 +168,11 @@ trait AccountService {
Repositories.filter(_.userName === userName.bind).delete
}
+ def getGroupNames(userName: String)(implicit s: Session): List[String] = {
+ List(userName) ++
+ Collaborators.filter(_.collaboratorName === userName.bind).sortBy(_.userName).map(_.userName).list
+ }
+
}
object AccountService extends AccountService
diff --git a/src/main/scala/service/IssuesService.scala b/src/main/scala/service/IssuesService.scala
index 5688fc78f..8795f0323 100644
--- a/src/main/scala/service/IssuesService.scala
+++ b/src/main/scala/service/IssuesService.scala
@@ -47,9 +47,9 @@ trait IssuesService {
* @param repos Tuple of the repository owner and the repository name
* @return the count of the search result
*/
- def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
+ def countIssue(condition: IssueSearchCondition, onlyPullRequest: Boolean,
repos: (String, String)*)(implicit s: Session): Int =
- Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first
+ Query(searchIssueQuery(repos, condition, onlyPullRequest).length).first
/**
* Returns the Map which contains issue count for each labels.
@@ -62,7 +62,7 @@ trait IssuesService {
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
filterUser: Map[String, String])(implicit s: Session): Map[String, Int] = {
- searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false)
+ searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), false)
.innerJoin(IssueLabels).on { (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
}
@@ -77,46 +77,22 @@ trait IssuesService {
}
.toMap
}
- /**
- * Returns list which contains issue count for each repository.
- * If the issue does not exist, its repository is not included in the result.
- *
- * @param condition the search condition
- * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
- * @param repos Tuple of the repository owner and the repository name
- * @return list which contains issue count for each repository
- */
- def countIssueGroupByRepository(
- condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
- repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = {
- searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest)
- .groupBy { t =>
- t.userName -> t.repositoryName
- }
- .map { case (repo, t) =>
- (repo._1, repo._2, t.length)
- }
- .sortBy(_._3 desc)
- .list
- }
/**
* Returns the search result against issues.
*
* @param condition the search condition
- * @param filterUser the filter user name (key is "all", "assigned", "created_by" or "not_created_by", value is the user name)
* @param pullRequest if true then returns only pull requests, false then returns only issues.
* @param offset the offset for pagination
* @param limit the limit for pagination
* @param repos Tuple of the repository owner and the repository name
* @return the search result (list of tuples which contain issue, labels and comment count)
*/
- def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], pullRequest: Boolean,
- offset: Int, limit: Int, repos: (String, String)*)
+ def searchIssue(condition: IssueSearchCondition, pullRequest: Boolean, offset: Int, limit: Int, repos: (String, String)*)
(implicit s: Session): List[IssueInfo] = {
// get issues and comment count and labels
- searchIssueQuery(repos, condition, filterUser, pullRequest)
+ searchIssueQuery(repos, condition, pullRequest)
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
.sortBy { case (t1, t2) =>
(condition.sort match {
@@ -157,23 +133,18 @@ trait IssuesService {
/**
* Assembles query for conditional issue searching.
*/
- private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition,
- filterUser: Map[String, String], pullRequest: Boolean)(implicit s: Session) =
+ private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, pullRequest: Boolean)(implicit s: Session) =
Issues filter { t1 =>
- condition.repo
- .map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
- .getOrElse (repos)
- .map { case (owner, repository) => t1.byRepository(owner, repository) }
- .foldLeft[Column[Boolean]](false) ( _ || _ ) &&
+ repos
+ .map { case (owner, repository) => t1.byRepository(owner, repository) }
+ .foldLeft[Column[Boolean]](false) ( _ || _ ) &&
(t1.closed === (condition.state == "closed").bind) &&
(t1.milestoneId === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
(t1.milestoneId.? isEmpty, condition.milestoneId == Some(None)) &&
- (t1.assignedUserName === filterUser("assigned").bind, filterUser.get("assigned").isDefined) &&
- (t1.openedUserName === filterUser("created_by").bind, filterUser.get("created_by").isDefined) &&
- (t1.openedUserName =!= filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) &&
(t1.assignedUserName === condition.assigned.get.bind, condition.assigned.isDefined) &&
(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
(t1.pullRequest === pullRequest.bind) &&
+ // Label filter
(IssueLabels filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
(t2.labelId in
@@ -181,7 +152,19 @@ trait IssuesService {
(t3.byRepository(t1.userName, t1.repositoryName)) &&
(t3.labelName inSetBind condition.labels)
} map(_.labelId)))
- } exists, condition.labels.nonEmpty)
+ } exists, condition.labels.nonEmpty) &&
+ // Visibility filter
+ (Repositories filter { t2 =>
+ (t2.byRepository(t1.userName, t1.repositoryName)) &&
+ (t2.isPrivate === (condition.visibility == Some("private")).bind)
+ } exists, condition.visibility.nonEmpty) &&
+ // Organization (group) filter
+ (t1.userName inSetBind condition.groups, condition.groups.nonEmpty) &&
+ // Mentioned filter
+ ((t1.openedUserName === condition.mentioned.get.bind) || t1.assignedUserName === condition.mentioned.get.bind ||
+ (IssueComments filter { t2 =>
+ (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === condition.mentioned.get.bind)
+ } exists), condition.mentioned.isDefined)
}
def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
@@ -340,31 +323,62 @@ object IssuesService {
milestoneId: Option[Option[Int]] = None,
author: Option[String] = None,
assigned: Option[String] = None,
- repo: Option[String] = None,
+ mentioned: Option[String] = None,
state: String = "open",
sort: String = "created",
- direction: String = "desc"){
+ direction: String = "desc",
+ visibility: Option[String] = None,
+ groups: Set[String] = Set.empty){
def isEmpty: Boolean = {
labels.isEmpty && milestoneId.isEmpty && author.isEmpty && assigned.isEmpty &&
- state == "open" && sort == "created" && direction == "desc"
+ state == "open" && sort == "created" && direction == "desc" && visibility.isEmpty
}
def nonEmpty: Boolean = !isEmpty
+ def toFilterString: String = (
+ List(
+ Some(s"is:${state}"),
+ author.map(author => s"author:${author}"),
+ assigned.map(assignee => s"assignee:${assignee}"),
+ mentioned.map(mentioned => s"mentions:${mentioned}")
+ ).flatten ++
+ labels.map(label => s"label:${label}") ++
+ List(
+ milestoneId.map { _ match {
+ case Some(x) => s"milestone:${milestoneId}"
+ case None => "no:milestone"
+ }},
+ (sort, direction) match {
+ case ("created" , "desc") => None
+ case ("created" , "asc" ) => Some("sort:created-asc")
+ case ("comments", "desc") => Some("sort:comments-desc")
+ case ("comments", "asc" ) => Some("sort:comments-asc")
+ case ("updated" , "desc") => Some("sort:updated-desc")
+ case ("updated" , "asc" ) => Some("sort:updated-asc")
+ },
+ visibility.map(visibility => s"visibility:${visibility}")
+ ).flatten ++
+ groups.map(group => s"group:${group}")
+ ).mkString(" ")
+
def toURL: String =
"?" + List(
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
- milestoneId.map { id => "milestone=" + (id match {
- case Some(x) => x.toString
- case None => "none"
- })},
- author .map(x => "author=" + urlEncode(x)),
- assigned.map(x => "assigned=" + urlEncode(x)),
- repo.map("for=" + urlEncode(_)),
+ milestoneId.map { _ match {
+ case Some(x) => "milestone=" + x
+ case None => "milestone=none"
+ }},
+ author .map(x => "author=" + urlEncode(x)),
+ assigned .map(x => "assigned=" + urlEncode(x)),
+ mentioned.map(x => "mentioned=" + urlEncode(x)),
Some("state=" + urlEncode(state)),
Some("sort=" + urlEncode(sort)),
- Some("direction=" + urlEncode(direction))).flatten.mkString("&")
+ Some("direction=" + urlEncode(direction)),
+ visibility.map(x => "visibility=" + urlEncode(x)),
+ if(groups.isEmpty) None else Some("groups=" + urlEncode(groups.mkString(",")))
+ ).flatten.mkString("&")
}
@@ -375,19 +389,63 @@ object IssuesService {
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value)
}
+ /**
+ * Restores IssueSearchCondition instance from filter query.
+ */
+ def apply(filter: String, milestones: Map[String, Int]): IssueSearchCondition = {
+ val conditions = filter.split("[ \t]+").map { x =>
+ val dim = x.split(":")
+ dim(0) -> dim(1)
+ }.groupBy(_._1).map { case (key, values) =>
+ key -> values.map(_._2).toSeq
+ }
+
+ val (sort, direction) = conditions.get("sort").flatMap(_.headOption).getOrElse("created-desc") match {
+ case "created-asc" => ("created" , "asc" )
+ case "comments-desc" => ("comments", "desc")
+ case "comments-asc" => ("comments", "asc" )
+ case "updated-desc" => ("comments", "desc")
+ case "updated-asc" => ("comments", "asc" )
+ case _ => ("created" , "desc")
+ }
+
+ IssueSearchCondition(
+ conditions.get("label").map(_.toSet).getOrElse(Set.empty),
+ conditions.get("milestone").flatMap(_.headOption) match {
+ case None => None
+ case Some("none") => Some(None)
+ case Some(x) => milestones.get(x).map(x => Some(x))
+ },
+ conditions.get("author").flatMap(_.headOption),
+ conditions.get("assignee").flatMap(_.headOption),
+ conditions.get("mentions").flatMap(_.headOption),
+ conditions.get("is").getOrElse(Seq.empty).filter(x => x == "open" || x == "closed").headOption.getOrElse("open"),
+ sort,
+ direction,
+ conditions.get("visibility").flatMap(_.headOption),
+ conditions.get("group").map(_.toSet).getOrElse(Set.empty)
+ )
+ }
+
+ /**
+ * Restores IssueSearchCondition instance from request parameters.
+ */
def apply(request: HttpServletRequest): IssueSearchCondition =
IssueSearchCondition(
param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
- param(request, "milestone").map{
+ param(request, "milestone").map {
case "none" => None
case x => x.toIntOpt
},
param(request, "author"),
param(request, "assigned"),
- param(request, "for"),
+ param(request, "mentioned"),
param(request, "state", Seq("open", "closed")).getOrElse("open"),
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
- param(request, "direction", Seq("asc", "desc")).getOrElse("desc"))
+ param(request, "direction", Seq("asc", "desc")).getOrElse("desc"),
+ param(request, "visibility"),
+ param(request, "groups").map(_.split(",").toSet).getOrElse(Set.empty)
+ )
def page(request: HttpServletRequest) = try {
val i = param(request, "page").getOrElse("1").toInt
diff --git a/src/main/scala/service/PullRequestService.scala b/src/main/scala/service/PullRequestService.scala
index d5bcb0b2b..9a3239b4c 100644
--- a/src/main/scala/service/PullRequestService.scala
+++ b/src/main/scala/service/PullRequestService.scala
@@ -36,23 +36,23 @@ trait PullRequestService { self: IssuesService =>
.list
.map { x => PullRequestCount(x._1, x._2) }
- def getAllPullRequestCountGroupByUser(closed: Boolean, userName: String)(implicit s: Session): List[PullRequestCount] =
- PullRequests
- .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
- .innerJoin(Repositories).on { case ((t1, t2), t3) => t2.byRepository(t3.userName, t3.repositoryName) }
- .filter { case ((t1, t2), t3) =>
- (t2.closed === closed.bind) &&
- (
- (t3.isPrivate === false.bind) ||
- (t3.userName === userName.bind) ||
- (Collaborators.filter { t4 => t4.byRepository(t3.userName, t3.repositoryName) && (t4.collaboratorName === userName.bind)} exists)
- )
- }
- .groupBy { case ((t1, t2), t3) => t2.openedUserName }
- .map { case (userName, t) => userName -> t.length }
- .sortBy(_._2 desc)
- .list
- .map { x => PullRequestCount(x._1, x._2) }
+// def getAllPullRequestCountGroupByUser(closed: Boolean, userName: String)(implicit s: Session): List[PullRequestCount] =
+// PullRequests
+// .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
+// .innerJoin(Repositories).on { case ((t1, t2), t3) => t2.byRepository(t3.userName, t3.repositoryName) }
+// .filter { case ((t1, t2), t3) =>
+// (t2.closed === closed.bind) &&
+// (
+// (t3.isPrivate === false.bind) ||
+// (t3.userName === userName.bind) ||
+// (Collaborators.filter { t4 => t4.byRepository(t3.userName, t3.repositoryName) && (t4.collaboratorName === userName.bind)} exists)
+// )
+// }
+// .groupBy { case ((t1, t2), t3) => t2.openedUserName }
+// .map { case (userName, t) => userName -> t.length }
+// .sortBy(_._2 desc)
+// .list
+// .map { x => PullRequestCount(x._1, x._2) }
def createPullRequest(originUserName: String, originRepositoryName: String, issueId: Int,
originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String,
diff --git a/src/main/scala/service/RepositoryService.scala b/src/main/scala/service/RepositoryService.scala
index 31e363751..5594e7c4d 100644
--- a/src/main/scala/service/RepositoryService.scala
+++ b/src/main/scala/service/RepositoryService.scala
@@ -54,7 +54,6 @@ trait RepositoryService { self: AccountService =>
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueComments = IssueComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
- val activities = Activities .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val collaborators = Collaborators.filter(_.byRepository(oldUserName, oldRepositoryName)).list
Repositories.filter { t =>
@@ -69,11 +68,18 @@ trait RepositoryService { self: AccountService =>
t.requestRepositoryName === oldRepositoryName.bind
}.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName)
+ // Updates activity fk before deleting repository because activity is sorted by activityId
+ // and it can't be changed by deleting-and-inserting record.
+ Activities.filter(_.byRepository(oldUserName, oldRepositoryName)).list.foreach { activity =>
+ Activities.filter(_.activityId === activity.activityId.bind)
+ .map(x => (x.userName, x.repositoryName)).update(newUserName, newRepositoryName)
+ }
+
deleteRepository(oldUserName, oldRepositoryName)
- WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
- Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
- IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
+ WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
+ Milestones.insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
+ IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list
Issues.insertAll(issues.map { x => x.copy(
@@ -88,7 +94,7 @@ trait RepositoryService { self: AccountService =>
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueLabels .insertAll(issueLabels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
- Activities .insertAll(activities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
+
if(account.isGroupAccount){
Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*)
} else {
@@ -96,12 +102,9 @@ trait RepositoryService { self: AccountService =>
}
// Update activity messages
- val updateActivities = Activities.filter { t =>
- (t.message like s"%:${oldUserName}/${oldRepositoryName}]%") ||
- (t.message like s"%:${oldUserName}/${oldRepositoryName}#%")
- }.map { t => t.activityId -> t.message }.list
-
- updateActivities.foreach { case (activityId, message) =>
+ Activities.filter { t =>
+ (t.message like s"%:${oldUserName}/${oldRepositoryName}]%") || (t.message like s"%:${oldUserName}/${oldRepositoryName}#%")
+ }.map { t => t.activityId -> t.message }.list.foreach { case (activityId, message) =>
Activities.filter(_.activityId === activityId.bind).map(_.message).update(
message
.replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]")
diff --git a/src/main/scala/service/SystemSettingsService.scala b/src/main/scala/service/SystemSettingsService.scala
index 9fbd78f2a..63f9ad1d4 100644
--- a/src/main/scala/service/SystemSettingsService.scala
+++ b/src/main/scala/service/SystemSettingsService.scala
@@ -12,6 +12,7 @@ trait SystemSettingsService {
def saveSystemSettings(settings: SystemSettings): Unit = {
defining(new java.util.Properties()){ props =>
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
+ settings.information.foreach(x => props.setProperty(Information, x))
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
props.setProperty(Gravatar, settings.gravatar.toString)
props.setProperty(Notification, settings.notification.toString)
@@ -60,6 +61,7 @@ trait SystemSettingsService {
}
SystemSettings(
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
+ getOptionValue[String](props, Information, None),
getValue(props, AllowAccountRegistration, false),
getValue(props, Gravatar, true),
getValue(props, Notification, false),
@@ -105,6 +107,7 @@ object SystemSettingsService {
case class SystemSettings(
baseUrl: Option[String],
+ information: Option[String],
allowAccountRegistration: Boolean,
gravatar: Boolean,
notification: Boolean,
@@ -147,6 +150,7 @@ object SystemSettingsService {
val DefaultLdapPort = 389
private val BaseURL = "base_url"
+ private val Information = "information"
private val AllowAccountRegistration = "allow_account_registration"
private val Gravatar = "gravatar"
private val Notification = "notification"
diff --git a/src/main/scala/servlet/AutoUpdateListener.scala b/src/main/scala/servlet/AutoUpdateListener.scala
index aff301042..911c05a85 100644
--- a/src/main/scala/servlet/AutoUpdateListener.scala
+++ b/src/main/scala/servlet/AutoUpdateListener.scala
@@ -53,6 +53,7 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
+ new Version(2, 5),
new Version(2, 4),
new Version(2, 3) {
override def update(conn: Connection): Unit = {
diff --git a/src/main/scala/servlet/GitRepositoryServlet.scala b/src/main/scala/servlet/GitRepositoryServlet.scala
index 012c6ea00..df55d46fd 100644
--- a/src/main/scala/servlet/GitRepositoryServlet.scala
+++ b/src/main/scala/servlet/GitRepositoryServlet.scala
@@ -134,8 +134,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
// Retrieve all issue count in the repository
val issueCount =
- countIssue(IssueSearchCondition(state = "open"), Map.empty, false, owner -> repository) +
- countIssue(IssueSearchCondition(state = "closed"), Map.empty, false, owner -> repository)
+ countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) +
+ countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository)
// Extract new commit and apply issue comment
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch
diff --git a/src/main/scala/util/JGitUtil.scala b/src/main/scala/util/JGitUtil.scala
index 0e0493daf..5ea43a3a1 100644
--- a/src/main/scala/util/JGitUtil.scala
+++ b/src/main/scala/util/JGitUtil.scala
@@ -14,7 +14,7 @@ import org.eclipse.jgit.treewalk.filter._
import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException}
import java.util.Date
-import org.eclipse.jgit.api.errors.NoHeadException
+import org.eclipse.jgit.api.errors.{JGitInternalException, InvalidRefNameException, RefAlreadyExistsException, NoHeadException}
import service.RepositoryService
import org.eclipse.jgit.dircache.DirCacheEntry
import org.slf4j.LoggerFactory
@@ -507,6 +507,17 @@ object JGitUtil {
}.find(_._1 != null)
}
+ def createBranch(git: Git, fromBranch: String, newBranch: String) = {
+ try {
+ git.branchCreate().setStartPoint(fromBranch).setName(newBranch).call()
+ Right("Branch created.")
+ } catch {
+ case e: RefAlreadyExistsException => Left("Sorry, that branch already exists.")
+ // JGitInternalException occurs when new branch name is 'a' and the branch whose name is 'a/*' exists.
+ case _: InvalidRefNameException | _: JGitInternalException => Left("Sorry, that name is invalid.")
+ }
+ }
+
def createDirCacheEntry(path: String, mode: FileMode, objectId: ObjectId): DirCacheEntry = {
val entry = new DirCacheEntry(path)
entry.setFileMode(mode)
diff --git a/src/main/scala/view/Markdown.scala b/src/main/scala/view/Markdown.scala
index 05120244e..08c3dd411 100644
--- a/src/main/scala/view/Markdown.scala
+++ b/src/main/scala/view/Markdown.scala
@@ -9,6 +9,7 @@ import org.pegdown.ast._
import org.pegdown.LinkRenderer.Rendering
import java.text.Normalizer
import java.util.Locale
+import java.util.regex.Pattern
import scala.collection.JavaConverters._
import service.{RequestCache, WikiService}
@@ -18,17 +19,23 @@ object Markdown {
* Converts Markdown of Wiki pages to HTML.
*/
def toHtml(markdown: String, repository: service.RepositoryService.RepositoryInfo,
- enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): String = {
+ enableWikiLink: Boolean, enableRefsLink: Boolean,
+ enableTaskList: Boolean = false, hasWritePermission: Boolean = false)(implicit context: app.Context): String = {
// escape issue id
- val source = if(enableRefsLink){
+ val s = if(enableRefsLink){
markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2")
} else markdown
+ // escape task list
+ val source = if(enableTaskList){
+ GitBucketHtmlSerializer.escapeTaskList(s)
+ } else s
+
val rootNode = new PegDownProcessor(
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS
).parseMarkdown(source.toCharArray)
- new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink).toHtml(rootNode)
+ new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission).toHtml(rootNode)
}
}
@@ -82,7 +89,9 @@ class GitBucketHtmlSerializer(
markdown: String,
repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
- enableRefsLink: Boolean
+ enableRefsLink: Boolean,
+ enableTaskList: Boolean,
+ hasWritePermission: Boolean
)(implicit val context: app.Context) extends ToHtmlSerializer(
new GitBucketLinkRender(context, repository, enableWikiLink),
Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava
@@ -143,7 +152,10 @@ class GitBucketHtmlSerializer(
override def visit(node: TextNode): Unit = {
// convert commit id and username to link.
- val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText
+ val t = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText
+
+ // convert task list to checkbox.
+ val text = if(enableTaskList) GitBucketHtmlSerializer.convertCheckBox(t, hasWritePermission) else t
if (abbreviations.isEmpty) {
printer.print(text)
@@ -151,6 +163,28 @@ class GitBucketHtmlSerializer(
printWithAbbreviations(text)
}
}
+
+ override def visit(node: BulletListNode): Unit = {
+ if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) {
+ printer.println().print("""""").indent(+2)
+ visitChildren(node)
+ printer.indent(-2).println().print(" ")
+ } else {
+ printIndentedTag(node, "ul")
+ }
+ }
+
+ override def visit(node: ListItemNode): Unit = {
+ if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) {
+ printer.println()
+ printer.print("""""")
+ visitChildren(node)
+ printer.print(" ")
+ } else {
+ printer.println()
+ printTag(node, "li")
+ }
+ }
}
object GitBucketHtmlSerializer {
@@ -163,4 +197,14 @@ object GitBucketHtmlSerializer {
val noSpecialChars = StringUtil.urlEncode(normalized)
noSpecialChars.toLowerCase(Locale.ENGLISH)
}
+
+ def escapeTaskList(text: String): String = {
+ Pattern.compile("""^( *)- \[([x| ])\] """, Pattern.MULTILINE).matcher(text).replaceAll("$1* task:$2: ")
+ }
+
+ def convertCheckBox(text: String, hasWritePermission: Boolean): String = {
+ val disabled = if (hasWritePermission) "" else "disabled"
+ text.replaceAll("task:x:", """ ")
+ .replaceAll("task: :", """ ")
+ }
}
diff --git a/src/main/scala/view/helpers.scala b/src/main/scala/view/helpers.scala
index 78f658f62..19dd2e860 100644
--- a/src/main/scala/view/helpers.scala
+++ b/src/main/scala/view/helpers.scala
@@ -1,5 +1,5 @@
package view
-import java.util.{Date, TimeZone}
+import java.util.{Locale, Date, TimeZone}
import java.text.SimpleDateFormat
import play.twirl.api.Html
import util.StringUtil
@@ -15,6 +15,47 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
*/
def datetime(date: Date): String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date)
+ val timeUnits = List(
+ (1000L, "second"),
+ (1000L * 60, "minute"),
+ (1000L * 60 * 60, "hour"),
+ (1000L * 60 * 60 * 24, "day"),
+ (1000L * 60 * 60 * 24 * 30, "month"),
+ (1000L * 60 * 60 * 24 * 365, "year")
+ ).reverse
+
+ /**
+ * Format java.util.Date to "x {seconds/minutes/hours/days/months/years} ago"
+ */
+ def datetimeAgo(date: Date): String = {
+ val duration = new Date().getTime - date.getTime
+ timeUnits.find(tuple => duration / tuple._1 > 0) match {
+ case Some((unitValue, unitString)) =>
+ val value = duration / unitValue
+ s"${value} ${unitString}${if (value > 1) "s" else ""} ago"
+ case None => "just now"
+ }
+ }
+
+ /**
+ *
+ * Format java.util.Date to "x {seconds/minutes/hours/days} ago"
+ * If duration over 1 month, format to "d MMM (yyyy)"
+ *
+ */
+ def datetimeAgoRecentOnly(date: Date): String = {
+ val duration = new Date().getTime - date.getTime
+ timeUnits.find(tuple => duration / tuple._1 > 0) match {
+ case Some((_, "month")) => s"on ${new SimpleDateFormat("d MMM", Locale.ENGLISH).format(date)}"
+ case Some((_, "year")) => s"on ${new SimpleDateFormat("d MMM yyyy", Locale.ENGLISH).format(date)}"
+ case Some((unitValue, unitString)) =>
+ val value = duration / unitValue
+ s"${value} ${unitString}${if (value > 1) "s" else ""} ago"
+ case None => "just now"
+ }
+ }
+
+
/**
* Format java.util.Date to "yyyy-MM-dd'T'hh:mm:ss'Z'".
*/
@@ -48,8 +89,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
* Converts Markdown of Wiki pages to HTML.
*/
def markdown(value: String, repository: service.RepositoryService.RepositoryInfo,
- enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html =
- Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink))
+ enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean = false, hasWritePermission: Boolean = false)(implicit context: app.Context): Html =
+ Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission))
def renderMarkup(filePath: List[String], fileContent: String, branch: String,
repository: service.RepositoryService.RepositoryInfo,
diff --git a/src/main/twirl/account/repositories.scala.html b/src/main/twirl/account/repositories.scala.html
index 1ad52dfa8..8a3706b28 100644
--- a/src/main/twirl/account/repositories.scala.html
+++ b/src/main/twirl/account/repositories.scala.html
@@ -25,7 +25,7 @@
@if(repository.repository.description.isDefined){
@repository.repository.description
}
- Last updated: @datetime(repository.repository.lastActivityDate)
+ Updated @helper.html.datetimeago(repository.repository.lastActivityDate)
}
diff --git a/src/main/twirl/admin/system.scala.html b/src/main/twirl/admin/system.scala.html
index 3ef154f4a..9379672ef 100644
--- a/src/main/twirl/admin/system.scala.html
+++ b/src/main/twirl/admin/system.scala.html
@@ -31,6 +31,14 @@
You can use this property to adjust URL difference between the reverse proxy and GitBucket.
+
+
+
+ Information (HTML is available)
+
+
+
+
diff --git a/src/main/twirl/admin/users/user.scala.html b/src/main/twirl/admin/users/user.scala.html
index fb022c0c2..1f2585777 100644
--- a/src/main/twirl/admin/users/user.scala.html
+++ b/src/main/twirl/admin/users/user.scala.html
@@ -16,6 +16,9 @@
Disable
+
+
+
}
@if(account.map(_.password.nonEmpty).getOrElse(true)){
diff --git a/src/main/twirl/dashboard/header.scala.html b/src/main/twirl/dashboard/header.scala.html
new file mode 100644
index 000000000..01c5bfc2e
--- /dev/null
+++ b/src/main/twirl/dashboard/header.scala.html
@@ -0,0 +1,74 @@
+@(openCount: Int,
+ closedCount: Int,
+ condition: service.IssuesService.IssueSearchCondition,
+ groups: List[String])(implicit context: app.Context)
+@import context._
+@import view.helpers._
+
+
+
+ @openCount Open
+
+
+
+ @closedCount Closed
+
+
+
\ No newline at end of file
diff --git a/src/main/twirl/dashboard/issues.scala.html b/src/main/twirl/dashboard/issues.scala.html
index f6f9bbb71..898ac6b12 100644
--- a/src/main/twirl/dashboard/issues.scala.html
+++ b/src/main/twirl/dashboard/issues.scala.html
@@ -1,50 +1,16 @@
-@(listparts: play.twirl.api.Html,
- allCount: Int,
- assignedCount: Int,
- createdByCount: Int,
- repositories: List[(String, String, Int)],
+@(issues: List[service.IssuesService.IssueInfo],
+ page: Int,
+ openCount: Int,
+ closedCount: Int,
condition: service.IssuesService.IssueSearchCondition,
- filter: String)(implicit context: app.Context)
+ filter: String,
+ groups: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
-@html.main("Your Issues"){
-
+@html.main("Issues"){
@dashboard.html.tab("issues")
-
-
- @listparts
+
+ @issuesnavi(filter, "issues", condition)
+ @issueslist(issues, page, openCount, closedCount, condition, filter, groups)
-
}
diff --git a/src/main/twirl/dashboard/issueslist.scala.html b/src/main/twirl/dashboard/issueslist.scala.html
index 4f27ea8c4..16840b744 100644
--- a/src/main/twirl/dashboard/issueslist.scala.html
+++ b/src/main/twirl/dashboard/issueslist.scala.html
@@ -3,182 +3,65 @@
openCount: Int,
closedCount: Int,
condition: service.IssuesService.IssueSearchCondition,
- collaborators: List[String] = Nil,
- milestones: List[model.Milestone] = Nil,
- labels: List[model.Label] = Nil,
- repository: Option[service.RepositoryService.RepositoryInfo] = None,
- hasWritePermission: Boolean = false)(implicit context: app.Context)
+ filter: String,
+ groups: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@import service.IssuesService.IssueInfo
-
- @if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
-
- Clear milestone and label filters
-
- }
- @if(condition.repo.isDefined){
-
- Clear filter on @condition.repo
-
- }
-
- @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 7, condition.toURL)
-
-
- @helper.html.dropdown(
- value = (condition.sort, condition.direction) match {
- case ("created" , "desc") => "Newest"
- case ("created" , "asc" ) => "Oldest"
- case ("comments", "desc") => "Most commented"
- case ("comments", "asc" ) => "Least commented"
- case ("updated" , "desc") => "Recently updated"
- case ("updated" , "asc" ) => "Least recently updated"
- },
- prefix = "Sort",
- mini = false
- ){
-
-
- @helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
-
-
-
-
- @helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
-
-
-
-
- @helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
-
-
-
-
- @helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
-
-
-
-
- @helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
-
-
-
-
- @helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
-
-
- }
-
- @if(issues.isEmpty){
-
-
- No issues to show.
- @if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
- Clear active filters.
- } else {
- @if(repository.isDefined){
- Create a new issue.
- }
- }
-
-
+@*
+
+*@
+
+
+
+ @dashboard.html.header(openCount, closedCount, condition, groups)
+
+
+ @issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
+
+
+ @if(issue.isPullRequest){
+
} else {
- @if(hasWritePermission){
-
-
-
- @{if(condition.state == "open") "Close" else "Reopen"}
-
- @helper.html.dropdown("Label") {
- @labels.map { label =>
-
-
-
-
- @label.labelName
-
-
- }
- }
- @helper.html.dropdown("Assignee") {
- Clear assignee
- @collaborators.map { collaborator =>
- @avatar(collaborator, 20) @collaborator
- }
- }
- @helper.html.dropdown("Milestone") {
- Clear this milestone
- @milestones.map { milestone =>
-
-
- @milestone.title
-
- @milestone.dueDate.map { dueDate =>
- @if(isPast(dueDate)){
-
Due by @date(dueDate)
- } else {
-
Due by @date(dueDate)
- }
- }.getOrElse {
-
No due date
- }
-
-
-
- }
- }
-
-
+
+ }
+ @issue.repositoryName ・
+ @if(issue.isPullRequest){
+ @issue.title
+ } else {
+ @issue.title
+ }
+ @labels.map { label =>
+ @label.labelName
+ }
+
+ @issue.assignedUserName.map { userName =>
+ @avatar(userName, 20, tooltip = true)
+ }
+ @if(commentCount > 0){
+
+ } else {
+
+ }
+
+
+ #@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
+ @milestone.map { milestone =>
+
@milestone
}
- }
- @issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
-
-
- @if(hasWritePermission){
-
-
- }
- @if(issue.isPullRequest){
-
- } else {
-
- }
- @if(repository.isEmpty){
- @issue.repositoryName ・
- }
- @if(issue.isPullRequest){
- @issue.title
- } else {
- @issue.title
- }
- @labels.map { label =>
- @label.labelName
- }
-
- @issue.assignedUserName.map { userName =>
- @avatar(userName, 20, tooltip = true)
- }
- #@issue.issueId
-
-
- Opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
- @if(commentCount > 0){
-
- }
-
- @if(hasWritePermission){
-
- }
-
-
- }
-
-
- @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
-
-
+
+
+ }
+
+
+ @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
+
diff --git a/src/main/twirl/dashboard/issuesnavi.scala.html b/src/main/twirl/dashboard/issuesnavi.scala.html
new file mode 100644
index 000000000..ec15e7416
--- /dev/null
+++ b/src/main/twirl/dashboard/issuesnavi.scala.html
@@ -0,0 +1,22 @@
+@(filter: String,
+ active: String,
+ condition: service.IssuesService.IssueSearchCondition)(implicit context: app.Context)
+@import context._
+@import view.helpers._
+
diff --git a/src/main/twirl/dashboard/pulls.scala.html b/src/main/twirl/dashboard/pulls.scala.html
index 25caafec6..0e2eff640 100644
--- a/src/main/twirl/dashboard/pulls.scala.html
+++ b/src/main/twirl/dashboard/pulls.scala.html
@@ -1,42 +1,16 @@
-@(listparts: play.twirl.api.Html,
- counts: List[service.PullRequestService.PullRequestCount],
- repositories: List[(String, String, Int)],
+@(issues: List[service.IssuesService.IssueInfo],
+ page: Int,
+ openCount: Int,
+ closedCount: Int,
condition: service.IssuesService.IssueSearchCondition,
- filter: String)(implicit context: app.Context)
+ filter: String,
+ groups: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
-@html.main("Your Issues"){
-
+@html.main("Pull Requests"){
@dashboard.html.tab("pulls")
-
-
- @listparts
+
+ @issuesnavi(filter, "pulls", condition)
+ @pullslist(issues, page, openCount, closedCount, condition, filter, groups)
-
}
diff --git a/src/main/twirl/dashboard/pullslist.scala.html b/src/main/twirl/dashboard/pullslist.scala.html
index 46343b46e..1208816f2 100644
--- a/src/main/twirl/dashboard/pullslist.scala.html
+++ b/src/main/twirl/dashboard/pullslist.scala.html
@@ -3,99 +3,65 @@
openCount: Int,
closedCount: Int,
condition: service.IssuesService.IssueSearchCondition,
- repository: Option[service.RepositoryService.RepositoryInfo],
- hasWritePermission: Boolean)(implicit context: app.Context)
+ filter: String,
+ groups: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@import service.IssuesService.IssueInfo
-
- @repository.map { repository =>
- @if(hasWritePermission){
-
- @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.PullRequestService.PullRequestLimit, 7, condition.toURL)
-
New pull request
+@*
+