diff --git a/README.md b/README.md index 4a44fce..a5f6406 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,18 @@ 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 1d50b97..9372a97 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 b470f0e..7c44529 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 @@ 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 b6b7bb9..9f6efcf 100644 --- a/src/main/scala/app/IssuesController.scala +++ b/src/main/scala/app/IssuesController.scala @@ -9,7 +9,6 @@ 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 @@ )(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 @@ 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 @@ 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 @@ // 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 c079f01..ff87241 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.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 @@ 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 @@ 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 4291730..5b5b11b 100644 --- a/src/main/scala/app/RepositoryViewerController.scala +++ b/src/main/scala/app/RepositoryViewerController.scala @@ -77,7 +77,9 @@ 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 @@ 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 } } @@ -240,6 +242,24 @@ }) /** + * 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. */ get("/:owner/:repository/delete/*")(collaboratorsOnly { repository => @@ -331,7 +351,8 @@ 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 27b18fa..57e4eb3 100644 --- a/src/main/scala/app/SystemSettingsController.scala +++ b/src/main/scala/app/SystemSettingsController.scala @@ -21,6 +21,7 @@ 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 6125039..d98d703 100644 --- a/src/main/scala/app/UserManagementController.scala +++ b/src/main/scala/app/UserManagementController.scala @@ -49,7 +49,7 @@ "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 @@ } } + 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 b815c54..c502eb7 100644 --- a/src/main/scala/service/AccountService.scala +++ b/src/main/scala/service/AccountService.scala @@ -168,6 +168,11 @@ 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 5688fc7..8795f03 100644 --- a/src/main/scala/service/IssuesService.scala +++ b/src/main/scala/service/IssuesService.scala @@ -47,9 +47,9 @@ * @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 @@ 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 @@ } .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 @@ /** * 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 @@ (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 @@ 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 @@ 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 d5bcb0b..9a3239b 100644 --- a/src/main/scala/service/PullRequestService.scala +++ b/src/main/scala/service/PullRequestService.scala @@ -36,23 +36,23 @@ .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 31e3637..5594e7c 100644 --- a/src/main/scala/service/RepositoryService.scala +++ b/src/main/scala/service/RepositoryService.scala @@ -54,7 +54,6 @@ 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 @@ 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 @@ 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 @@ } // 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 9fbd78f..63f9ad1 100644 --- a/src/main/scala/service/SystemSettingsService.scala +++ b/src/main/scala/service/SystemSettingsService.scala @@ -12,6 +12,7 @@ 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 @@ } 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 @@ case class SystemSettings( baseUrl: Option[String], + information: Option[String], allowAccountRegistration: Boolean, gravatar: Boolean, notification: Boolean, @@ -147,6 +150,7 @@ 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 aff3010..911c05a 100644 --- a/src/main/scala/servlet/AutoUpdateListener.scala +++ b/src/main/scala/servlet/AutoUpdateListener.scala @@ -53,6 +53,7 @@ * 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 012c6ea..df55d46 100644 --- a/src/main/scala/servlet/GitRepositoryServlet.scala +++ b/src/main/scala/servlet/GitRepositoryServlet.scala @@ -134,8 +134,8 @@ // 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 0e0493d..5ea43a3 100644 --- a/src/main/scala/util/JGitUtil.scala +++ b/src/main/scala/util/JGitUtil.scala @@ -14,7 +14,7 @@ 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 @@ }.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 0512024..08c3dd4 100644 --- a/src/main/scala/view/Markdown.scala +++ b/src/main/scala/view/Markdown.scala @@ -9,6 +9,7 @@ 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 @@ * 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 @@ 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 @@ 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 @@ printWithAbbreviations(text) } } + + override def visit(node: BulletListNode): Unit = { + if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) { + printer.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 @@ 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 78f658f..19dd2e8 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 @@ */ 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 @@ * 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 1ad52df..8a3706b 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 3ef154f..9379672 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.

    + + +
    + +
    + +
    +
    diff --git a/src/main/twirl/admin/users/user.scala.html b/src/main/twirl/admin/users/user.scala.html index fb022c0..1f25857 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 0000000..01c5bfc --- /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 + + +
    + @helper.html.dropdown("Visibility", flat = true){ +
  • + + @helper.html.checkicon(condition.visibility == Some("private")) + Private repository only + +
  • +
  • + + @helper.html.checkicon(condition.visibility == Some("public")) + Public repository only + +
  • + } + @helper.html.dropdown("Organization", flat = true){ + @groups.map { group => +
  • + + @helper.html.checkicon(condition.groups.contains(group)) + @avatar(group, 20) @group + +
  • + } + } + @helper.html.dropdown("Sort", flat = true){ +
  • + + @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 + +
  • + } +
    \ 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 f6f9bbb..898ac6b 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 4f27ea8..16840b7 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. - } - } -
    + + + + @issues.map { case IssueInfo(issue, labels, milestone, commentCount) => + + - - + + } + @issue.repositoryName ・ + @if(issue.isPullRequest){ + @issue.title + } else { + @issue.title + } + @labels.map { label => + @label.labelName + } + + @issue.assignedUserName.map { userName => + @avatar(userName, 20, tooltip = true) } - } - @issues.map { case IssueInfo(issue, labels, milestone, commentCount) => - - - - } -
    + @dashboard.html.header(openCount, closedCount, condition, groups) +
    + @if(issue.isPullRequest){ + } else { - @if(hasWritePermission){ -
    -
    - -
    - @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 - } -
    -
    -
  • - } - } -
    - @if(hasWritePermission){ - - } -
    -
    - @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL) + @if(commentCount > 0){ + + @commentCount + + } else { + + @commentCount + + } + +
    + #@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate) + @milestone.map { milestone => + @milestone + }
    -
    - + + + } + +
    + @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 0000000..ec15e74 --- /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 25caafe..0e2eff6 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 46343b4..1208816 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 +@* +
    \ No newline at end of file + +
    + @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.PullRequestService.PullRequestLimit, 10, condition.toURL) +
    diff --git a/src/main/twirl/dashboard/tab.scala.html b/src/main/twirl/dashboard/tab.scala.html index 1a43265..3cb3bf0 100644 --- a/src/main/twirl/dashboard/tab.scala.html +++ b/src/main/twirl/dashboard/tab.scala.html @@ -1,13 +1,47 @@ @(active: String = "")(implicit context: app.Context) @import context._ @import view.helpers._ - +
    +
    + + + News Feed + + @if(loginAccount.isDefined){ + + + Pull Requests + + + + Issues + + } +
    +
    + \ No newline at end of file diff --git a/src/main/twirl/helper/activities.scala.html b/src/main/twirl/helper/activities.scala.html index 980eaf5..9bb3f8d 100644 --- a/src/main/twirl/helper/activities.scala.html +++ b/src/main/twirl/helper/activities.scala.html @@ -62,7 +62,7 @@ @detailActivity(activity: model.Activity, image: String) = {
    -
    @datetime(activity.activityDate)
    +
    @helper.html.datetimeago(activity.activityDate)
    @avatar(activity.activityUserName, 16) @activityMessage(activity.message) @@ -76,7 +76,7 @@ @customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = {
    -
    @datetime(activity.activityDate)
    +
    @helper.html.datetimeago(activity.activityDate)
    @avatar(activity.activityUserName, 16) @activityMessage(activity.message) @@ -91,7 +91,7 @@
    @avatar(activity.activityUserName, 16) @activityMessage(activity.message) - @datetime(activity.activityDate) + @helper.html.datetimeago(activity.activityDate)
    } diff --git a/src/main/twirl/helper/branchcontrol.scala.html b/src/main/twirl/helper/branchcontrol.scala.html new file mode 100644 index 0000000..5381688 --- /dev/null +++ b/src/main/twirl/helper/branchcontrol.scala.html @@ -0,0 +1,62 @@ +@(branch: String = "", + repository: service.RepositoryService.RepositoryInfo, + hasWritePermission: Boolean)(body: Html)(implicit context: app.Context) +@import context._ +@import view.helpers._ +@helper.html.dropdown( + value = if(branch.length == 40) branch.substring(0, 10) else branch, + prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree", + mini = true +) { +
  • Switch branches
  • +
  • + @body + @if(hasWritePermission) { + + } +} + diff --git a/src/main/twirl/helper/datetimeago.scala.html b/src/main/twirl/helper/datetimeago.scala.html new file mode 100644 index 0000000..3b68f34 --- /dev/null +++ b/src/main/twirl/helper/datetimeago.scala.html @@ -0,0 +1,10 @@ +@(latestUpdatedDate: java.util.Date, + recentOnly: Boolean = true) +@import view.helpers._ + + @if(recentOnly){ + @datetimeAgoRecentOnly(latestUpdatedDate) + }else{ + @datetimeAgo(latestUpdatedDate) + } + diff --git a/src/main/twirl/helper/error.scala.html b/src/main/twirl/helper/error.scala.html new file mode 100644 index 0000000..00f43e2 --- /dev/null +++ b/src/main/twirl/helper/error.scala.html @@ -0,0 +1,7 @@ +@(error: Option[Any]) +@if(error.isDefined){ +
    + + @error +
    +} \ No newline at end of file diff --git a/src/main/twirl/helper/information.scala.html b/src/main/twirl/helper/information.scala.html index d8bcff5..ff382a2 100644 --- a/src/main/twirl/helper/information.scala.html +++ b/src/main/twirl/helper/information.scala.html @@ -1,7 +1,7 @@ @(info: Option[Any]) @if(info.isDefined){
    - + @info
    } diff --git a/src/main/twirl/helper/preview.scala.html b/src/main/twirl/helper/preview.scala.html index b6f37c7..3c0e13e 100644 --- a/src/main/twirl/helper/preview.scala.html +++ b/src/main/twirl/helper/preview.scala.html @@ -1,4 +1,4 @@ -@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean, +@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean, hasWritePermission: Boolean, style: String = "", placeholder: String = "Leave a comment", elastic: Boolean = false)(implicit context: app.Context) @import context._ @import view.helpers._ @@ -38,7 +38,8 @@ $.post('@url(repository)/_preview', { content : $('#content').val(), enableWikiLink : @enableWikiLink, - enableRefsLink : @enableRefsLink + enableRefsLink : @enableRefsLink, + enableTaskList : @enableTaskList }, function(data){ $('#preview-area').html(data); prettyPrint(); diff --git a/src/main/twirl/index.scala.html b/src/main/twirl/index.scala.html index ec8b7ce..816569f 100644 --- a/src/main/twirl/index.scala.html +++ b/src/main/twirl/index.scala.html @@ -4,13 +4,23 @@ @import context._ @import view.helpers._ @main("GitBucket"){ + @dashboard.html.tab()
    - @dashboard.html.tab()
    +
    + activities +
    @helper.html.activities(activities)
    +
    + @settings.information.map { information => +
    + + @Html(information) +
    + } @if(loginAccount.isEmpty){ @signinform(settings) } else { diff --git a/src/main/twirl/issues/commentform.scala.html b/src/main/twirl/issues/commentform.scala.html index 4ca951e..04169c7 100644 --- a/src/main/twirl/issues/commentform.scala.html +++ b/src/main/twirl/issues/commentform.scala.html @@ -10,7 +10,7 @@
    @avatar(loginAccount.get.userName, 48)
    - @helper.html.preview(repository, "", false, true, "width: 635px; height: 100px; max-height: 150px;", elastic = true) + @helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 635px; height: 100px; max-height: 150px;", elastic = true)
    @@ -28,4 +28,4 @@ $('').attr('name', 'action').val($(this).val().toLowerCase()).appendTo('form'); }); }); - \ No newline at end of file + diff --git a/src/main/twirl/issues/commentlist.scala.html b/src/main/twirl/issues/commentlist.scala.html index 979062f..b351a1d 100644 --- a/src/main/twirl/issues/commentlist.scala.html +++ b/src/main/twirl/issues/commentlist.scala.html @@ -8,7 +8,7 @@
    @avatar(issue.openedUserName, 48)
    - @user(issue.openedUserName, styleClass="username strong") commented on @datetime(issue.registeredDate) + @user(issue.openedUserName, styleClass="username strong") commented @helper.html.datetimeago(issue.registeredDate) @if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){ @@ -16,7 +16,7 @@
    - @markdown(issue.content getOrElse "No description provided.", repository, false, true) + @markdown(issue.content getOrElse "No description provided.", repository, false, true, true, hasWritePermission)
    @@ -32,7 +32,7 @@ } else { @if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request } } - on @datetime(comment.registeredDate) + @helper.html.datetimeago(comment.registeredDate) @if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" && @@ -46,7 +46,7 @@ @if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){ @defining(comment.content.substring(comment.content.length - 40)){ id => - @markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true) + @markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true, true, hasWritePermission) } } else { @if(comment.action == "refer"){ @@ -54,7 +54,7 @@ Issue #@issueId: @rest.mkString(":") } } else { - @markdown(comment.content, repository, false, true) + @markdown(comment.content, repository, false, true, true, hasWritePermission) } }
    @@ -70,7 +70,7 @@ } else { @pullreq.map(_.userName):@pullreq.map(_.branch) to @pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch) } - @datetime(comment.registeredDate) + @helper.html.datetimeago(comment.registeredDate)
    } @if(comment.action == "close" || comment.action == "close_comment"){ @@ -78,9 +78,9 @@ Closed @avatar(comment.commentedUserName, 20) @if(issue.isPullRequest){ - @user(comment.commentedUserName, styleClass="username strong") closed the pull request @datetime(comment.registeredDate) + @user(comment.commentedUserName, styleClass="username strong") closed the pull request @helper.html.datetimeago(comment.registeredDate) } else { - @user(comment.commentedUserName, styleClass="username strong") closed the issue @datetime(comment.registeredDate) + @user(comment.commentedUserName, styleClass="username strong") closed the issue @helper.html.datetimeago(comment.registeredDate) }
    } @@ -88,14 +88,14 @@
    Reopened @avatar(comment.commentedUserName, 20) - @user(comment.commentedUserName, styleClass="username strong") reopened the issue @datetime(comment.registeredDate) + @user(comment.commentedUserName, styleClass="username strong") reopened the issue @helper.html.datetimeago(comment.registeredDate)
    } @if(comment.action == "delete_branch"){
    Deleted @avatar(comment.commentedUserName, 20) - @user(comment.commentedUserName, styleClass="username strong") deleted the @pullreq.map(_.requestBranch) branch @datetime(comment.registeredDate) + @user(comment.commentedUserName, styleClass="username strong") deleted the @pullreq.map(_.requestBranch) branch @helper.html.datetimeago(comment.registeredDate)
    } } @@ -134,5 +134,67 @@ } return false; }); + + var extractMarkdown = function(data){ + $('body').append('
    '); + $('#tmp').html(data); + var markdown = $('#tmp textarea').val(); + $('#tmp').remove(); + return markdown; + }; + + var replaceTaskList = function(issueContentHtml, checkboxes) { + var ss = [], + markdown = extractMarkdown(issueContentHtml), + xs = markdown.split(/- \[[x| ]\]/g); + for (var i=0; i \ No newline at end of file + diff --git a/src/main/twirl/issues/create.scala.html b/src/main/twirl/issues/create.scala.html index cf91159..4062651 100644 --- a/src/main/twirl/issues/create.scala.html +++ b/src/main/twirl/issues/create.scala.html @@ -7,7 +7,7 @@ @import view.helpers._ @html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){ @html.menu("issues", repository){ - @tab("issues", false, repository) + @navigation("issues", false, repository)


    @@ -57,7 +57,7 @@

    - @helper.html.preview(repository, "", false, true, "width: 565px; height: 200px; max-height: 250px;", elastic = true) + @helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 565px; height: 200px; max-height: 250px;", elastic = true)
    diff --git a/src/main/twirl/issues/issue.scala.html b/src/main/twirl/issues/issue.scala.html index 3b45ebf..3b7f125 100644 --- a/src/main/twirl/issues/issue.scala.html +++ b/src/main/twirl/issues/issue.scala.html @@ -28,7 +28,7 @@ Open } - @user(issue.openedUserName, styleClass="username strong") opened this issue on @datetime(issue.registeredDate) - @defining( + @user(issue.openedUserName, styleClass="username strong") opened this issue @helper.html.datetimeago(issue.registeredDate) - @defining( comments.filter( _.action.contains("comment") ).size ){ count => @count @plural(count, "comment") diff --git a/src/main/twirl/issues/labels/list.scala.html b/src/main/twirl/issues/labels/list.scala.html index f21a88b..471dcf5 100644 --- a/src/main/twirl/issues/labels/list.scala.html +++ b/src/main/twirl/issues/labels/list.scala.html @@ -6,8 +6,8 @@ @import view.helpers._ @html.main(s"Labels - ${repository.owner}/${repository.name}"){ @html.menu("issues", repository){ - @issues.html.tab("labels", hasWritePermission, repository) -   + @issues.html.navigation("labels", hasWritePermission, repository) +
    diff --git a/src/main/twirl/issues/list.scala.html b/src/main/twirl/issues/list.scala.html index cd00ac3..b1e36b3 100644 --- a/src/main/twirl/issues/list.scala.html +++ b/src/main/twirl/issues/list.scala.html @@ -13,7 +13,7 @@ @import view.helpers._ @html.main((if(target == "issues") "Issues" else "Pull requests") + s" - ${repository.owner}/${repository.name}", Some(repository)){ @html.menu(target, repository){ - @tab(target, true, repository) + @navigation(target, true, repository, Some(condition)) @listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission) @if(hasWritePermission){ diff --git a/src/main/twirl/issues/listparts.scala.html b/src/main/twirl/issues/listparts.scala.html index d3b5a11..1ff944f 100644 --- a/src/main/twirl/issues/listparts.scala.html +++ b/src/main/twirl/issues/listparts.scala.html @@ -12,6 +12,7 @@ @import context._ @import view.helpers._ @import service.IssuesService.IssueInfo +
    @if(condition.nonEmpty){ } -  - +
    @@ -203,7 +203,7 @@ }
    - #@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate) + #@issue.issueId opened @helper.html.datetimeago(issue.registeredDate) by @user(issue.openedUserName, styleClass="username") @milestone.map { milestone => @milestone } diff --git a/src/main/twirl/issues/milestones/edit.scala.html b/src/main/twirl/issues/milestones/edit.scala.html index a2c328e..f876cf4 100644 --- a/src/main/twirl/issues/milestones/edit.scala.html +++ b/src/main/twirl/issues/milestones/edit.scala.html @@ -7,7 +7,7 @@

    New milestone

    Create a new milestone to help organize your issues and pull requests.
    } else { - @issues.html.tab("milestones", false, repository) + @issues.html.navigation("milestones", false, repository)

    }
    diff --git a/src/main/twirl/issues/milestones/list.scala.html b/src/main/twirl/issues/milestones/list.scala.html index 8770832..7254a5c 100644 --- a/src/main/twirl/issues/milestones/list.scala.html +++ b/src/main/twirl/issues/milestones/list.scala.html @@ -6,8 +6,8 @@ @import view.helpers._ @html.main(s"Milestones - ${repository.owner}/${repository.name}"){ @html.menu("issues", repository){ - @issues.html.tab("milestones", hasWritePermission, repository) -   + @issues.html.navigation("milestones", hasWritePermission, repository) +
    - + }
    @@ -34,7 +34,7 @@ @milestone.title
    @if(milestone.closedDate.isDefined){ - Closed @datetime(milestone.closedDate.get) + Closed @helper.html.datetimeago(milestone.closedDate.get) } else { @milestone.dueDate.map { dueDate => @if(isPast(dueDate)){ diff --git a/src/main/twirl/issues/navigation.scala.html b/src/main/twirl/issues/navigation.scala.html new file mode 100644 index 0000000..9c1ee8c --- /dev/null +++ b/src/main/twirl/issues/navigation.scala.html @@ -0,0 +1,58 @@ +@(active: String, + newButton: Boolean, + repository: service.RepositoryService.RepositoryInfo, + condition: Option[service.IssuesService.IssueSearchCondition] = None)(implicit context: app.Context) +@import context._ +@import view.helpers._ + diff --git a/src/main/twirl/issues/tab.scala.html b/src/main/twirl/issues/tab.scala.html deleted file mode 100644 index 80ee66f..0000000 --- a/src/main/twirl/issues/tab.scala.html +++ /dev/null @@ -1,30 +0,0 @@ -@(active: String, newButton: Boolean, - repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ - diff --git a/src/main/twirl/menu.scala.html b/src/main/twirl/menu.scala.html index 8076c82..7ac1dc8 100644 --- a/src/main/twirl/menu.scala.html +++ b/src/main/twirl/menu.scala.html @@ -1,7 +1,9 @@ @(active: String, repository: service.RepositoryService.RepositoryInfo, id: Option[String] = None, - expand: Boolean = false)(body: Html)(implicit context: app.Context) + expand: Boolean = false, + info: Option[Any] = None, + error: Option[Any] = None)(body: Html)(implicit context: app.Context) @import context._ @import view.helpers._ @@ -31,6 +33,8 @@ }
    + @helper.html.information(info) + @helper.html.error(error) @if(repository.commitCount > 0){
    diff --git a/src/main/twirl/pulls/compare.scala.html b/src/main/twirl/pulls/compare.scala.html index b99e5b2..7d900ab 100644 --- a/src/main/twirl/pulls/compare.scala.html +++ b/src/main/twirl/pulls/compare.scala.html @@ -58,7 +58,7 @@
    - @helper.html.preview(repository, "", false, true, "width: 580px; height: 200px;") + @helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 580px; height: 200px;") diff --git a/src/main/twirl/pulls/pullreq.scala.html b/src/main/twirl/pulls/pullreq.scala.html index 35cebb3..495379b 100644 --- a/src/main/twirl/pulls/pullreq.scala.html +++ b/src/main/twirl/pulls/pullreq.scala.html @@ -20,7 +20,7 @@ Merged @user(comment.commentedUserName, styleClass="username strong") merged @commits.size @plural(commits.size, "commit") into @pullreq.userName:@pullreq.branch from @pullreq.requestUserName:@pullreq.requestBranch - at @datetime(comment.registeredDate) + @helper.html.datetimeago(comment.registeredDate) }.getOrElse { Closed @user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit") diff --git a/src/main/twirl/repo/blob.scala.html b/src/main/twirl/repo/blob.scala.html index 2c895ac..549d2e8 100644 --- a/src/main/twirl/repo/blob.scala.html +++ b/src/main/twirl/repo/blob.scala.html @@ -9,10 +9,10 @@ @html.main(s"${repository.owner}/${repository.name}", Some(repository)) { @html.menu("code", repository){
    - @helper.html.dropdown( - value = if(branch.length == 40) branch.substring(0, 10) else branch, - prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree", - mini = true + @helper.html.branchcontrol( + branch, + repository, + hasWritePermission ){ @repository.branchList.map { x =>
  • @helper.html.checkicon(x == branch) @x
  • @@ -34,7 +34,7 @@
    @avatar(latestCommit, 20) @user(latestCommit.authorName, latestCommit.authorEmailAddress, "username strong") - @datetime(latestCommit.commitTime) + @helper.html.datetimeago(latestCommit.commitTime) @link(latestCommit.summary, repository)
    diff --git a/src/main/twirl/repo/branches.scala.html b/src/main/twirl/repo/branches.scala.html index 214c37c..9857684 100644 --- a/src/main/twirl/repo/branches.scala.html +++ b/src/main/twirl/repo/branches.scala.html @@ -22,7 +22,7 @@ }
    - @datetime(latestUpdateDate) + @helper.html.datetimeago(latestUpdateDate, false) @if(repository.repository.defaultBranch == branchName){ diff --git a/src/main/twirl/repo/commit.scala.html b/src/main/twirl/repo/commit.scala.html index a022236..9b7c2f0 100644 --- a/src/main/twirl/repo/commit.scala.html +++ b/src/main/twirl/repo/commit.scala.html @@ -68,13 +68,13 @@
    @avatar(commit, 20) @user(commit.authorName, commit.authorEmailAddress, "username strong") - authored on @datetime(commit.authorTime) + authored @helper.html.datetimeago(commit.authorTime)
    @if(commit.isDifferentFromAuthor) {
    @user(commit.committerName, commit.committerEmailAddress, "username strong") - committed on @datetime(commit.commitTime) + committed @helper.html.datetimeago(commit.commitTime)
    } diff --git a/src/main/twirl/repo/commits.scala.html b/src/main/twirl/repo/commits.scala.html index be2b02f..443ab07 100644 --- a/src/main/twirl/repo/commits.scala.html +++ b/src/main/twirl/repo/commits.scala.html @@ -3,16 +3,17 @@ repository: service.RepositoryService.RepositoryInfo, commits: Seq[Seq[util.JGitUtil.CommitInfo]], page: Int, - hasNext: Boolean)(implicit context: app.Context) + hasNext: Boolean, + hasWritePermission: Boolean)(implicit context: app.Context) @import context._ @import view.helpers._ @html.main(s"${repository.owner}/${repository.name}", Some(repository)) { @html.menu("code", repository){
    - @helper.html.dropdown( - value = if(branch.length == 40) branch.substring(0, 10) else branch, - prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree", - mini = true + @helper.html.branchcontrol( + branch, + repository, + hasWritePermission ){ @repository.branchList.map { x =>
  • @helper.html.checkicon(x == branch) @x
  • @@ -58,11 +59,11 @@ }
    @user(commit.authorName, commit.authorEmailAddress, "username") - authored @datetime(commit.authorTime) + authored @helper.html.datetimeago(commit.authorTime) @if(commit.isDifferentFromAuthor) { @user(commit.committerName, commit.committerEmailAddress, "username") - committed @datetime(commit.authorTime) + committed @helper.html.datetimeago(commit.authorTime) }
    diff --git a/src/main/twirl/repo/files.scala.html b/src/main/twirl/repo/files.scala.html index 2235d6e..96e4a61 100644 --- a/src/main/twirl/repo/files.scala.html +++ b/src/main/twirl/repo/files.scala.html @@ -4,16 +4,18 @@ latestCommit: util.JGitUtil.CommitInfo, files: List[util.JGitUtil.FileInfo], readme: Option[(List[String], String)], - hasWritePermission: Boolean)(implicit context: app.Context) + hasWritePermission: Boolean, + info: Option[Any] = None, + error: Option[Any] = None)(implicit context: app.Context) @import context._ @import view.helpers._ @html.main(s"${repository.owner}/${repository.name}", Some(repository)) { - @html.menu("code", repository, Some(branch), pathList.isEmpty){ + @html.menu("code", repository, Some(branch), pathList.isEmpty, info, error){
    - @helper.html.dropdown( - value = if(branch.length == 40) branch.substring(0, 10) else branch, - prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree", - mini = true + @helper.html.branchcontrol( + branch, + repository, + hasWritePermission ){ @repository.branchList.map { x =>
  • @helper.html.checkicon(x == branch) @x
  • @@ -47,13 +49,13 @@
    @avatar(latestCommit, 20) @user(latestCommit.authorName, latestCommit.authorEmailAddress, "username strong") - authored on @datetime(latestCommit.authorTime) + authored @helper.html.datetimeago(latestCommit.authorTime)
    @if(latestCommit.isDifferentFromAuthor) {
    @user(latestCommit.committerName, latestCommit.committerEmailAddress, "username strong") - committed on @datetime(latestCommit.commitTime) + committed @helper.html.datetimeago(latestCommit.commitTime)
    }
    @@ -106,7 +108,7 @@ @link(file.message, repository) [@user(file.author, file.mailAddress)]
    @datetime(file.time)@helper.html.datetimeago(file.time, false)
    diff --git a/src/main/twirl/repo/tags.scala.html b/src/main/twirl/repo/tags.scala.html index e5b0fa9..cd611fb 100644 --- a/src/main/twirl/repo/tags.scala.html +++ b/src/main/twirl/repo/tags.scala.html @@ -14,7 +14,7 @@ @repository.tags.reverse.map { tag =>
    @tag.name@datetime(tag.time)@helper.html.datetimeago(tag.time, false) @tag.id.substring(0, 10) ZIP diff --git a/src/main/twirl/search/code.scala.html b/src/main/twirl/search/code.scala.html index 20b08a6..04d0a19 100644 --- a/src/main/twirl/search/code.scala.html +++ b/src/main/twirl/search/code.scala.html @@ -16,7 +16,7 @@ @files.drop((page - 1) * CodeLimit).take(CodeLimit).map { file =>
    @file.path
    -
    Latest commit at @datetime(file.lastModified)
    +
    Last commited @helper.html.datetimeago(file.lastModified)
    @Html(file.highlightText)
    } diff --git a/src/main/twirl/search/issues.scala.html b/src/main/twirl/search/issues.scala.html index d381091..7a9c17a 100644 --- a/src/main/twirl/search/issues.scala.html +++ b/src/main/twirl/search/issues.scala.html @@ -22,7 +22,7 @@ }
    Opened by @issue.openedUserName - at @datetime(issue.registeredDate) + @helper.html.datetimeago(issue.registeredDate) @if(issue.commentCount > 0){   @issue.commentCount @plural(issue.commentCount, "comment") } diff --git a/src/main/twirl/wiki/edit.scala.html b/src/main/twirl/wiki/edit.scala.html index b7ca333..b918cc0 100644 --- a/src/main/twirl/wiki/edit.scala.html +++ b/src/main/twirl/wiki/edit.scala.html @@ -10,19 +10,19 @@

    Editing @if(pageName.isEmpty){New Page} else {@pageName}

  • -
    - @if(page.isDefined){ - View Page - Delete Page - Page History - } +
    + @if(page.isDefined){ + Delete Page + Page History + } + New Page
  • - @helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, "width: 850px; height: 400px;", "") + @helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, false, false, "width: 850px; height: 400px;", "") @@ -36,4 +36,4 @@ return confirm('Are you sure you want to delete this page?'); }); }); - \ No newline at end of file + diff --git a/src/main/twirl/wiki/history.scala.html b/src/main/twirl/wiki/history.scala.html index bd2ba49..a773160 100644 --- a/src/main/twirl/wiki/history.scala.html +++ b/src/main/twirl/wiki/history.scala.html @@ -16,33 +16,39 @@
  • -
    +
    @if(pageName.isEmpty){ @if(loginAccount.isDefined){ New Page } } else { - View Page @if(loginAccount.isDefined){ Edit Page + New Page } }
  • + + + @commits.map { commit => }
    +
    Revisions
    +
    + +
    +
    @avatar(commit, 20) @user(commit.authorName, commit.authorEmailAddress) - @datetime(commit.authorTime): @commit.shortMessage + @helper.html.datetimeago(commit.authorTime): @commit.shortMessage
    - -