Compare commits

...

27 Commits
2.5 ... 2.6

Author SHA1 Message Date
Naoki Takezoe
823c52e941 Update README.md 2014-11-24 03:18:10 +09:00
Naoki Takezoe
7f42007648 (refs #507)Small fix about pull request UI 2014-11-23 14:59:03 +09:00
Naoki Takezoe
7214ef21d2 (refs #559)Fix merged message 2014-11-23 11:45:22 +09:00
Naoki Takezoe
18a4492975 Update README.md 2014-11-23 11:19:24 +09:00
Naoki Takezoe
99f73b1016 (refs #560)Replace build status badge with Travis 2014-11-23 01:29:38 +09:00
Naoki Takezoe
0c1ce6a088 (refs #560)Add travis configuration 2014-11-23 01:19:26 +09:00
Naoki Takezoe
ae6291ab83 Update version to 2.6 2014-11-22 22:36:27 +09:00
Naoki Takezoe
617fcf7c99 Fix compilation error in test 2014-11-22 22:34:27 +09:00
Naoki Takezoe
9df4a74837 (refs #507)Applying new UI to pull request detail page has been completed. 2014-11-22 22:30:44 +09:00
Naoki Takezoe
966d4251be (refs #507)Applying new UI to pull request detail page 2014-11-22 22:02:33 +09:00
Naoki Takezoe
84b2e9cdcd Fix compilation error 2014-11-22 21:47:34 +09:00
Naoki Takezoe
e29d63c91a Merge branch 'master' into newui-for-pullreq 2014-11-22 21:46:51 +09:00
Naoki Takezoe
805d2b8e79 (refs #530)Don't re-sort activities by repository renaming. 2014-11-22 21:40:32 +09:00
Naoki Takezoe
9983fd1292 Update README.md 2014-11-22 12:57:33 +09:00
Naoki Takezoe
1de202e927 (refs #554)Search box bug fix 2014-11-19 07:17:37 +09:00
Naoki Takezoe
4eb9f4a485 (refs #551)Adjust wiki buttons 2014-11-17 17:08:51 +09:00
Naoki Takezoe
a8801e4e41 (refs #432)Show the information message at the top page 2014-11-17 00:52:51 +09:00
Naoki Takezoe
ee1c84dbf2 (refs #508)Remove filter parameter 2014-11-16 20:22:36 +09:00
Naoki Takezoe
e40e1fa6cd (refs #508)Add search filter box to the dashboard 2014-11-16 01:38:31 +09:00
Naoki Takezoe
055f648ea2 (refs #508)Remove repository filter 2014-11-11 13:10:07 +09:00
Naoki Takezoe
37a399c3a2 (refs #508)Fix line separator 2014-11-11 12:09:48 +09:00
Naoki Takezoe
bc0b11b60a (refs #508)Basic filter box implementation 2014-11-11 12:07:22 +09:00
Naoki Takezoe
65a1ca7146 (refs #508)Start to add search filter box 2014-11-11 03:19:51 +09:00
Naoki Takezoe
2293030d4e Merge pull request #541 from rlazoti/fix-dashboard
Fix pull request's view on dashboard
2014-11-09 00:21:39 +09:00
Rodrigo Lazoti
c83fab611e Remove the 'All' tab 2014-11-06 19:01:16 -02:00
Rodrigo Lazoti
29baf1223c Fix pull request's view on dashboard 2014-11-04 18:15:07 -02:00
shimamoto
ff4052f097 (refs #507) Fix the issue info of conversation page. 2014-10-19 23:39:25 +09:00
37 changed files with 490 additions and 243 deletions

3
.travis.yml Normal file
View File

@@ -0,0 +1,3 @@
language: scala
scala:
- 2.11.2

View File

@@ -1,4 +1,4 @@
GitBucket [![Gitter chat](https://badges.gitter.im/takezoe/gitbucket.png)](https://gitter.im/takezoe/gitbucket) [![Build Status](https://buildhive.cloudbees.com/job/takezoe/job/gitbucket/badge/icon)](https://buildhive.cloudbees.com/job/takezoe/job/gitbucket/) GitBucket [![Gitter chat](https://badges.gitter.im/takezoe/gitbucket.png)](https://gitter.im/takezoe/gitbucket) [![Build Status](https://travis-ci.org/takezoe/gitbucket.svg?branch=master)](https://travis-ci.org/takezoe/gitbucket)
========= =========
GitBucket is the easily installable Github clone written with Scala. GitBucket is the easily installable Github clone written with Scala.
@@ -80,6 +80,13 @@ Run the following commands in `Terminal` to
Release Notes Release Notes
-------- --------
### 2.6 - 24 Nov 2014
- Search box at issues and pull requests
- Information from administrator
- Pull request UI has been updated
- Move to TravisCI from Buildhive
- Some bug fix and improvements
### 2.5 - 4 Nov 2014 ### 2.5 - 4 Nov 2014
- New Dashboard - New Dashboard
- Change datetime format - Change datetime format

View File

@@ -1,8 +1,9 @@
package app package app
import service._ import service._
import util.{UsersAuthenticator, Keys} import util.{StringUtil, UsersAuthenticator, Keys}
import util.Implicits._ import util.Implicits._
import service.IssuesService.IssueSearchCondition
class DashboardController extends DashboardControllerBase class DashboardController extends DashboardControllerBase
with IssuesService with PullRequestService with RepositoryService with AccountService with IssuesService with PullRequestService with RepositoryService with AccountService
@@ -12,8 +13,21 @@ trait DashboardControllerBase extends ControllerBase {
self: IssuesService with PullRequestService with RepositoryService with AccountService self: IssuesService with PullRequestService with RepositoryService with AccountService
with UsersAuthenticator => with UsersAuthenticator =>
get("/dashboard/issues/repos")(usersOnly { 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") searchIssues("created_by")
}
}) })
get("/dashboard/issues/assigned")(usersOnly { get("/dashboard/issues/assigned")(usersOnly {
@@ -29,70 +43,92 @@ trait DashboardControllerBase extends ControllerBase {
}) })
get("/dashboard/pulls")(usersOnly { 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 { get("/dashboard/pulls/created_by")(usersOnly {
searchPullRequests("created_by", None) searchPullRequests("created_by")
})
get("/dashboard/pulls/assigned")(usersOnly {
searchPullRequests("assigned")
}) })
get("/dashboard/pulls/mentioned")(usersOnly { get("/dashboard/pulls/mentioned")(usersOnly {
searchPullRequests("mentioned", None) searchPullRequests("mentioned")
}) })
get("/dashboard/pulls/public")(usersOnly { private def getOrCreateCondition(key: String, filter: String, userName: String) = {
searchPullRequests("not_created_by", None) 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()))
get("/dashboard/pulls/for/:owner/:repository")(usersOnly { filter match {
searchPullRequests("all", Some(params("owner") + "/" + params("repository"))) 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) = { private def searchIssues(filter: String) = {
import IssuesService._ 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 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 userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
dashboard.html.issues( dashboard.html.issues(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*), searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
page, page,
countIssue(condition.copy(state = "open" ), filterUser, false, userRepos: _*), countIssue(condition.copy(state = "open" ), false, userRepos: _*),
countIssue(condition.copy(state = "closed"), filterUser, false, userRepos: _*), countIssue(condition.copy(state = "closed"), false, userRepos: _*),
condition, filter match {
case "assigned" => condition.copy(assigned = Some(userName))
case "mentioned" => condition.copy(mentioned = Some(userName))
case _ => condition.copy(author = Some(userName))
},
filter, filter,
getGroupNames(userName)) getGroupNames(userName))
} }
private def searchPullRequests(filter: String, repository: Option[String]) = { private def searchPullRequests(filter: String) = {
import IssuesService._ import IssuesService._
import PullRequestService._ 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 userName = context.loginAccount.get.userName
val condition = getOrCreateCondition(Keys.Session.DashboardPulls, filter, userName)
val allRepos = getAllRepositories(userName) val allRepos = getAllRepositories(userName)
val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
dashboard.html.pulls( dashboard.html.pulls(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*), searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
page, page,
countIssue(condition.copy(state = "open" ), filterUser, true, allRepos: _*), countIssue(condition.copy(state = "open" ), true, allRepos: _*),
countIssue(condition.copy(state = "closed"), filterUser, true, allRepos: _*), countIssue(condition.copy(state = "closed"), true, allRepos: _*),
condition, filter match {
case "assigned" => condition.copy(assigned = Some(userName))
case "mentioned" => condition.copy(mentioned = Some(userName))
case _ => condition.copy(author = Some(userName))
},
filter, filter,
getGroupNames(userName)) getGroupNames(userName))
} }

View File

@@ -9,7 +9,6 @@ import util.Implicits._
import util.ControlUtil._ import util.ControlUtil._
import org.scalatra.Ok import org.scalatra.Ok
import model.Issue import model.Issue
import plugin.PluginSystem
class IssuesController extends IssuesControllerBase class IssuesController extends IssuesControllerBase
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
@@ -50,7 +49,12 @@ trait IssuesControllerBase extends ControllerBase {
)(IssueStateForm.apply) )(IssueStateForm.apply)
get("/:owner/:repository/issues")(referrersOnly { repository => get("/:owner/:repository/issues")(referrersOnly { 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) searchIssues(repository)
}
}) })
get("/:owner/:repository/issues/:id")(referrersOnly { repository => get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
@@ -390,19 +394,25 @@ trait IssuesControllerBase extends ControllerBase {
// retrieve search condition // retrieve search condition
val condition = session.putAndGet(sessionKey, val condition = session.putAndGet(sessionKey,
if(request.hasQueryString) IssueSearchCondition(request) if(request.hasQueryString){
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition()) 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.html.list(
"issues", "issues",
searchIssue(condition, Map.empty, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page, page,
(getCollaborators(owner, repoName) :+ owner).sorted, (getCollaborators(owner, repoName) :+ owner).sorted,
getMilestones(owner, repoName), getMilestones(owner, repoName),
getLabels(owner, repoName), getLabels(owner, repoName),
countIssue(condition.copy(state = "open" ), Map.empty, false, owner -> repoName), countIssue(condition.copy(state = "open" ), false, owner -> repoName),
countIssue(condition.copy(state = "closed"), Map.empty, false, owner -> repoName), countIssue(condition.copy(state = "closed"), false, owner -> repoName),
condition, condition,
repository, repository,
hasWritePermission(owner, repoName, context.loginAccount)) hasWritePermission(owner, repoName, context.loginAccount))

View File

@@ -1,6 +1,6 @@
package app package app
import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier, Keys} import util._
import util.Directory._ import util.Directory._
import util.Implicits._ import util.Implicits._
import util.ControlUtil._ import util.ControlUtil._
@@ -18,6 +18,9 @@ import org.slf4j.LoggerFactory
import org.eclipse.jgit.merge.MergeStrategy import org.eclipse.jgit.merge.MergeStrategy
import org.eclipse.jgit.errors.NoMergeBaseException import org.eclipse.jgit.errors.NoMergeBaseException
import service.WebHookService.WebHookPayload import service.WebHookService.WebHookPayload
import util.JGitUtil.DiffInfo
import scala.Some
import util.JGitUtil.CommitInfo
class PullRequestsController extends PullRequestsControllerBase class PullRequestsController extends PullRequestsControllerBase
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService 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) case class MergeForm(message: String)
get("/:owner/:repository/pulls")(referrersOnly { repository => get("/:owner/:repository/pulls")(referrersOnly { 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) searchPullRequests(None, repository)
}
}) })
get("/:owner/:repository/pull/:id")(referrersOnly { repository => get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
@@ -460,13 +468,13 @@ trait PullRequestsControllerBase extends ControllerBase {
issues.html.list( issues.html.list(
"pulls", "pulls",
searchIssue(condition, Map.empty, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
page, page,
(getCollaborators(owner, repoName) :+ owner).sorted, (getCollaborators(owner, repoName) :+ owner).sorted,
getMilestones(owner, repoName), getMilestones(owner, repoName),
getLabels(owner, repoName), getLabels(owner, repoName),
countIssue(condition.copy(state = "open" ), Map.empty, true, owner -> repoName), countIssue(condition.copy(state = "open" ), true, owner -> repoName),
countIssue(condition.copy(state = "closed"), Map.empty, true, owner -> repoName), countIssue(condition.copy(state = "closed"), true, owner -> repoName),
condition, condition,
repository, repository,
hasWritePermission(owner, repoName, context.loginAccount)) hasWritePermission(owner, repoName, context.loginAccount))

View File

@@ -21,6 +21,7 @@ trait SystemSettingsControllerBase extends ControllerBase {
private val form = mapping( private val form = mapping(
"baseUrl" -> trim(label("Base URL", optional(text()))), "baseUrl" -> trim(label("Base URL", optional(text()))),
"information" -> trim(label("Information", optional(text()))),
"allowAccountRegistration" -> trim(label("Account registration", boolean())), "allowAccountRegistration" -> trim(label("Account registration", boolean())),
"gravatar" -> trim(label("Gravatar", boolean())), "gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())), "notification" -> trim(label("Notification", boolean())),

View File

@@ -47,9 +47,9 @@ trait IssuesService {
* @param repos Tuple of the repository owner and the repository name * @param repos Tuple of the repository owner and the repository name
* @return the count of the search result * @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 = 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. * Returns the Map which contains issue count for each labels.
@@ -62,7 +62,7 @@ trait IssuesService {
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition, def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
filterUser: Map[String, String])(implicit s: Session): Map[String, Int] = { 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) => .innerJoin(IssueLabels).on { (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
} }
@@ -78,46 +78,21 @@ trait IssuesService {
.toMap .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. * Returns the search result against issues.
* *
* @param condition the search condition * @param condition the search condition
* @param filterUser the filter user name (key is "all", "assigned", "created_by", "not_created_by" or "mentioned", value is the user name)
* @param pullRequest if true then returns only pull requests, false then returns only issues. * @param pullRequest if true then returns only pull requests, false then returns only issues.
* @param offset the offset for pagination * @param offset the offset for pagination
* @param limit the limit for pagination * @param limit the limit for pagination
* @param repos Tuple of the repository owner and the repository name * @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) * @return the search result (list of tuples which contain issue, labels and comment count)
*/ */
def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], pullRequest: Boolean, def searchIssue(condition: IssueSearchCondition, pullRequest: Boolean, offset: Int, limit: Int, repos: (String, String)*)
offset: Int, limit: Int, repos: (String, String)*)
(implicit s: Session): List[IssueInfo] = { (implicit s: Session): List[IssueInfo] = {
// get issues and comment count and labels // 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) } .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
.sortBy { case (t1, t2) => .sortBy { case (t1, t2) =>
(condition.sort match { (condition.sort match {
@@ -158,20 +133,14 @@ trait IssuesService {
/** /**
* Assembles query for conditional issue searching. * Assembles query for conditional issue searching.
*/ */
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, pullRequest: Boolean)(implicit s: Session) =
filterUser: Map[String, String], pullRequest: Boolean)(implicit s: Session) =
Issues filter { t1 => Issues filter { t1 =>
condition.repo repos
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
.getOrElse (repos)
.map { case (owner, repository) => t1.byRepository(owner, repository) } .map { case (owner, repository) => t1.byRepository(owner, repository) }
.foldLeft[Column[Boolean]](false) ( _ || _ ) && .foldLeft[Column[Boolean]](false) ( _ || _ ) &&
(t1.closed === (condition.state == "closed").bind) && (t1.closed === (condition.state == "closed").bind) &&
(t1.milestoneId === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) && (t1.milestoneId === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
(t1.milestoneId.? isEmpty, condition.milestoneId == Some(None)) && (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.assignedUserName === condition.assigned.get.bind, condition.assigned.isDefined) &&
(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) && (t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
(t1.pullRequest === pullRequest.bind) && (t1.pullRequest === pullRequest.bind) &&
@@ -192,10 +161,10 @@ trait IssuesService {
// Organization (group) filter // Organization (group) filter
(t1.userName inSetBind condition.groups, condition.groups.nonEmpty) && (t1.userName inSetBind condition.groups, condition.groups.nonEmpty) &&
// Mentioned filter // Mentioned filter
((t1.openedUserName === filterUser("mentioned").bind) || t1.assignedUserName === filterUser("mentioned").bind || ((t1.openedUserName === condition.mentioned.get.bind) || t1.assignedUserName === condition.mentioned.get.bind ||
(IssueComments filter { t2 => (IssueComments filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === filterUser("mentioned").bind) (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === condition.mentioned.get.bind)
} exists), filterUser.get("mentioned").isDefined) } exists), condition.mentioned.isDefined)
} }
def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String], def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
@@ -354,7 +323,7 @@ object IssuesService {
milestoneId: Option[Option[Int]] = None, milestoneId: Option[Option[Int]] = None,
author: Option[String] = None, author: Option[String] = None,
assigned: Option[String] = None, assigned: Option[String] = None,
repo: Option[String] = None, mentioned: Option[String] = None,
state: String = "open", state: String = "open",
sort: String = "created", sort: String = "created",
direction: String = "desc", direction: String = "desc",
@@ -368,16 +337,42 @@ object IssuesService {
def nonEmpty: Boolean = !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 = def toURL: String =
"?" + List( "?" + List(
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))), if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
milestoneId.map { id => "milestone=" + (id match { milestoneId.map { _ match {
case Some(x) => x.toString case Some(x) => "milestone=" + x
case None => "none" case None => "milestone=none"
})}, }},
author .map(x => "author=" + urlEncode(x)), author .map(x => "author=" + urlEncode(x)),
assigned.map(x => "assigned=" + urlEncode(x)), assigned .map(x => "assigned=" + urlEncode(x)),
repo.map("for=" + urlEncode(_)), mentioned.map(x => "mentioned=" + urlEncode(x)),
Some("state=" + urlEncode(state)), Some("state=" + urlEncode(state)),
Some("sort=" + urlEncode(sort)), Some("sort=" + urlEncode(sort)),
Some("direction=" + urlEncode(direction)), Some("direction=" + urlEncode(direction)),
@@ -394,6 +389,47 @@ object IssuesService {
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value) 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 = def apply(request: HttpServletRequest): IssueSearchCondition =
IssueSearchCondition( IssueSearchCondition(
param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty), param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
@@ -403,7 +439,7 @@ object IssuesService {
}, },
param(request, "author"), param(request, "author"),
param(request, "assigned"), param(request, "assigned"),
param(request, "for"), param(request, "mentioned"),
param(request, "state", Seq("open", "closed")).getOrElse("open"), param(request, "state", Seq("open", "closed")).getOrElse("open"),
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"), 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"),

View File

@@ -54,7 +54,6 @@ trait RepositoryService { self: AccountService =>
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueComments = IssueComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list val issueComments = IssueComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueLabels = IssueLabels .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 val collaborators = Collaborators.filter(_.byRepository(oldUserName, oldRepositoryName)).list
Repositories.filter { t => Repositories.filter { t =>
@@ -69,10 +68,17 @@ trait RepositoryService { self: AccountService =>
t.requestRepositoryName === oldRepositoryName.bind t.requestRepositoryName === oldRepositoryName.bind
}.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName) }.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) deleteRepository(oldUserName, oldRepositoryName)
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Milestones.insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*) IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list
@@ -88,7 +94,7 @@ trait RepositoryService { self: AccountService =>
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueLabels .insertAll(issueLabels .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){ if(account.isGroupAccount){
Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*) Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*)
} else { } else {
@@ -96,12 +102,9 @@ trait RepositoryService { self: AccountService =>
} }
// Update activity messages // Update activity messages
val updateActivities = Activities.filter { t => Activities.filter { t =>
(t.message like s"%:${oldUserName}/${oldRepositoryName}]%") || (t.message like s"%:${oldUserName}/${oldRepositoryName}]%") || (t.message like s"%:${oldUserName}/${oldRepositoryName}#%")
(t.message like s"%:${oldUserName}/${oldRepositoryName}#%") }.map { t => t.activityId -> t.message }.list.foreach { case (activityId, message) =>
}.map { t => t.activityId -> t.message }.list
updateActivities.foreach { case (activityId, message) =>
Activities.filter(_.activityId === activityId.bind).map(_.message).update( Activities.filter(_.activityId === activityId.bind).map(_.message).update(
message message
.replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]") .replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]")

View File

@@ -12,6 +12,7 @@ trait SystemSettingsService {
def saveSystemSettings(settings: SystemSettings): Unit = { def saveSystemSettings(settings: SystemSettings): Unit = {
defining(new java.util.Properties()){ props => defining(new java.util.Properties()){ props =>
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", ""))) 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(AllowAccountRegistration, settings.allowAccountRegistration.toString)
props.setProperty(Gravatar, settings.gravatar.toString) props.setProperty(Gravatar, settings.gravatar.toString)
props.setProperty(Notification, settings.notification.toString) props.setProperty(Notification, settings.notification.toString)
@@ -60,6 +61,7 @@ trait SystemSettingsService {
} }
SystemSettings( SystemSettings(
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")), getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
getOptionValue[String](props, Information, None),
getValue(props, AllowAccountRegistration, false), getValue(props, AllowAccountRegistration, false),
getValue(props, Gravatar, true), getValue(props, Gravatar, true),
getValue(props, Notification, false), getValue(props, Notification, false),
@@ -105,6 +107,7 @@ object SystemSettingsService {
case class SystemSettings( case class SystemSettings(
baseUrl: Option[String], baseUrl: Option[String],
information: Option[String],
allowAccountRegistration: Boolean, allowAccountRegistration: Boolean,
gravatar: Boolean, gravatar: Boolean,
notification: Boolean, notification: Boolean,
@@ -147,6 +150,7 @@ object SystemSettingsService {
val DefaultLdapPort = 389 val DefaultLdapPort = 389
private val BaseURL = "base_url" private val BaseURL = "base_url"
private val Information = "information"
private val AllowAccountRegistration = "allow_account_registration" private val AllowAccountRegistration = "allow_account_registration"
private val Gravatar = "gravatar" private val Gravatar = "gravatar"
private val Notification = "notification" private val Notification = "notification"

View File

@@ -53,6 +53,7 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version. * The history of versions. A head of this sequence is the current BitBucket version.
*/ */
val versions = Seq( val versions = Seq(
new Version(2, 6),
new Version(2, 5), new Version(2, 5),
new Version(2, 4), new Version(2, 4),
new Version(2, 3) { new Version(2, 3) {

View File

@@ -134,8 +134,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
// Retrieve all issue count in the repository // Retrieve all issue count in the repository
val issueCount = val issueCount =
countIssue(IssueSearchCondition(state = "open"), Map.empty, false, owner -> repository) + countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) +
countIssue(IssueSearchCondition(state = "closed"), Map.empty, false, owner -> repository) countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository)
// Extract new commit and apply issue comment // Extract new commit and apply issue comment
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch

View File

@@ -31,6 +31,14 @@
You can use this property to adjust URL difference between the reverse proxy and GitBucket. You can use this property to adjust URL difference between the reverse proxy and GitBucket.
</p> </p>
<!--====================================================================--> <!--====================================================================-->
<!-- Information -->
<!--====================================================================-->
<hr>
<label><span class="strong">Information</span> (HTML is available)</label>
<fieldset>
<textarea name="information" style="width: 600px; height: 100px;">@settings.information</textarea>
</fieldset>
<!--====================================================================-->
<!-- Account registration --> <!-- Account registration -->
<!--====================================================================--> <!--====================================================================-->
<hr> <hr>

View File

@@ -10,6 +10,7 @@
@html.main("Issues"){ @html.main("Issues"){
@dashboard.html.tab("issues") @dashboard.html.tab("issues")
<div class="container"> <div class="container">
@issuesnavi(filter, "issues", condition)
@issueslist(issues, page, openCount, closedCount, condition, filter, groups) @issueslist(issues, page, openCount, closedCount, condition, filter, groups)
</div> </div>
} }

View File

@@ -8,11 +8,13 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@import service.IssuesService.IssueInfo @import service.IssuesService.IssueInfo
@*
<ul class="nav nav-pills-group pull-left fill-width"> <ul class="nav nav-pills-group pull-left fill-width">
<li class="@if(filter == "created_by"){active} first"><a href="@path/dashboard/issues/created_by@condition.toURL">Created</a></li> <li class="@if(filter == "created_by"){active} first"><a href="@path/dashboard/issues/created_by@condition.toURL">Created</a></li>
<li class="@if(filter == "assigned"){active}"><a href="@path/dashboard/issues/assigned@condition.toURL">Assigned</a></li> <li class="@if(filter == "assigned"){active}"><a href="@path/dashboard/issues/assigned@condition.toURL">Assigned</a></li>
<li class="@if(filter == "mentioned"){active} last"><a href="@path/dashboard/issues/mentioned@condition.toURL">Mentioned</a></li> <li class="@if(filter == "mentioned"){active} last"><a href="@path/dashboard/issues/mentioned@condition.toURL">Mentioned</a></li>
</ul> </ul>
*@
<table class="table table-bordered table-hover table-issues"> <table class="table table-bordered table-hover table-issues">
<tr> <tr>
<th style="background-color: #eee;"> <th style="background-color: #eee;">

View File

@@ -0,0 +1,22 @@
@(filter: String,
active: String,
condition: service.IssuesService.IssueSearchCondition)(implicit context: app.Context)
@import context._
@import view.helpers._
<ul class="nav nav-pills-group pull-left fill-width">
<li class="@if(filter == "created_by"){active} first">
<a href="@path/dashboard/@active/created_by@condition.copy(author = None, assigned = None).toURL">Created</a>
</li>
<li class="@if(filter == "assigned"){active}">
<a href="@path/dashboard/@active/assigned@condition.copy(author = None, assigned = None).toURL">Assigned</a>
</li>
<li class="@if(filter == "mentioned"){active} last">
<a href="@path/dashboard/@active/mentioned@condition.copy(author = None, assigned = None).toURL">Mentioned</a>
</li>
<li class="pull-right">
<form method="GET" id="search-filter-form" action="@path/dashboard/@active" style="margin-bottom: 0px;">
<input type="text" id="search-filter-box" class="input-xlarge" name="q" style="height: 24px; width: 400px;"
value="is:@{if(active == "issues") "issue" else "pr"} @condition.toFilterString"/>
</form>
</li>
</ul>

View File

@@ -10,6 +10,7 @@
@html.main("Pull Requests"){ @html.main("Pull Requests"){
@dashboard.html.tab("pulls") @dashboard.html.tab("pulls")
<div class="container"> <div class="container">
@issueslist(issues, page, openCount, closedCount, condition, filter, groups) @issuesnavi(filter, "pulls", condition)
@pullslist(issues, page, openCount, closedCount, condition, filter, groups)
</div> </div>
} }

View File

@@ -8,11 +8,31 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@import service.IssuesService.IssueInfo @import service.IssuesService.IssueInfo
@*
<ul class="nav nav-pills-group pull-left fill-width"> <ul class="nav nav-pills-group pull-left fill-width">
<li class="@if(filter == "created_by"){active} first"><a href="@path/dashboard/pulls/created_by@condition.toURL">Created</a></li> <li class="@if(filter == "created_by"){active} first"><a href="@path/dashboard/pulls/created_by@condition.toURL">Created</a></li>
<li class="@if(filter == "assigned"){active}"><a href="@path/dashboard/pulls/assigned@condition.toURL">Assigned</a></li> <li class="@if(filter == "assigned"){active}"><a href="@path/dashboard/pulls/assigned@condition.toURL">Assigned</a></li>
<li class="@if(filter == "mentioned"){active} last"><a href="@path/dashboard/pulls/mentioned@condition.toURL">Mentioned</a></li> <li class="@if(filter == "mentioned"){active} last"><a href="@path/dashboard/pulls/mentioned@condition.toURL">Mentioned</a></li>
<li class="pull-right">
<div class="input-prepend" style="margin-bottom: 0px;">
<div class="btn-group">
<button type="button" class="btn dropdown-toggle" data-toggle="dropdown" style="height: 34px;">
Filter
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a href="?q=is:open">Open issues and pull requests</a></li>
<li><a href="?q=is:open+is:issue+author:@urlEncode(loginAccount.get.userName)">Your issues</a></li>
<li><a href="?q=is:open+is:pr+author:@urlEncode(loginAccount.get.userName)">Your pull requests</a></li>
<li><a href="?q=is:open+assignee:@urlEncode(loginAccount.get.userName)">Everything assigned to you</a></li>
<li><a href="?q=is:open+mentions:@urlEncode(loginAccount.get.userName)">Everything mentioning you</a></li>
</ul>
</div>
<input type="text" id="search-filter-box" class="input-xlarge" name="q" style="height: 24px;" value="is:@{if(active == "issues") "issue" else "pr"} @condition.toFilterString"/>
</div>
</li>
</ul> </ul>
*@
<table class="table table-bordered table-hover table-issues"> <table class="table table-bordered table-hover table-issues">
<tr> <tr>
<th style="background-color: #eee;"> <th style="background-color: #eee;">

View File

@@ -12,7 +12,7 @@
<img src="@assets/common/images/menu-pulls.png"> <img src="@assets/common/images/menu-pulls.png">
Pull Requests Pull Requests
</a> </a>
<a href="@path/dashboard/issues/repos" @if(active == "issues"){ class="active"}> <a href="@path/dashboard/issues" @if(active == "issues"){ class="active"}>
<img src="@assets/common/images/menu-issues.png"> <img src="@assets/common/images/menu-issues.png">
Issues Issues
</a> </a>

View File

@@ -13,7 +13,14 @@
</div> </div>
@helper.html.activities(activities) @helper.html.activities(activities)
</div> </div>
<div class="span4"> <div class="span4">
@settings.information.map { information =>
<div class="alert alert-info" style="background-color: white; color: #555; border-color: #4183c4; font-size: small; line-height: 120%;">
<button type="button" class="close" data-dismiss="alert">&times;</button>
@Html(information)
</div>
}
@if(loginAccount.isEmpty){ @if(loginAccount.isEmpty){
@signinform(settings) @signinform(settings)
} else { } else {

View File

@@ -68,7 +68,7 @@
@if(pullreq.get.requestUserName == repository.owner){ @if(pullreq.get.requestUserName == repository.owner){
<span class="label label-info monospace">@pullreq.map(_.branch)</span> from <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> <span class="label label-info monospace">@pullreq.map(_.branch)</span> from <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span>
} else { } else {
<span class="label label-info monospace">@pullreq.map(_.userName):@pullreq.map(_.branch)</span> to <span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span> <span class="label label-info monospace">@pullreq.map(_.userName):@pullreq.map(_.branch)</span> from <span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span>
} }
@helper.html.datetimeago(comment.registeredDate) @helper.html.datetimeago(comment.registeredDate)
</div> </div>

View File

@@ -7,7 +7,7 @@
@import view.helpers._ @import view.helpers._
@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){ @html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("issues", repository){ @html.menu("issues", repository){
@tab("issues", false, repository) @navigation("issues", false, repository)
<br/><br/><hr style="margin-bottom: 10px;"> <br/><br/><hr style="margin-bottom: 10px;">
<form action="@url(repository)/issues/new" method="POST" validate="true"> <form action="@url(repository)/issues/new" method="POST" validate="true">
<div class="row-fluid"> <div class="row-fluid">

View File

@@ -10,8 +10,16 @@
@import view.helpers._ @import view.helpers._
@html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){ @html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("issues", repository){ @html.menu("issues", repository){
<ul class="nav nav-tabs pull-left fill-width"> <div>
<li class="pull-left"> <div class="show-title pull-right">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a class="btn btn-small" href="#" id="edit">Edit</a>
}
<a class="btn btn-small btn-success" href="@url(repository)/issues/new">New issue</a>
</div>
<div class="edit-title pull-right" style="display: none;">
<a class="btn" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a>
</div>
<h1> <h1>
<span class="show-title"> <span class="show-title">
<span id="show-title">@issue.title</span> <span id="show-title">@issue.title</span>
@@ -19,9 +27,10 @@
</span> </span>
<span class="edit-title" style="display: none;"> <span class="edit-title" style="display: none;">
<span id="error-edit-title" class="error"></span> <span id="error-edit-title" class="error"></span>
<input type="text" class="span9" id="edit-title" value="@issue.title"/> <input type="text" style="width: 700px;" id="edit-title" value="@issue.title"/>
</span> </span>
</h1> </h1>
</div>
@if(issue.closed) { @if(issue.closed) {
<span class="label label-important issue-status">Closed</span> <span class="label label-important issue-status">Closed</span>
} else { } else {
@@ -35,19 +44,7 @@
} }
</span> </span>
<br/><br/> <br/><br/>
</li> <hr>
<li class="pull-right">
<div class="show-title">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a class="btn btn-small" href="#" id="edit">Edit</a>
}
<a class="btn btn-small btn-success" href="@url(repository)/issues/new">New issue</a>
</div>
<div class="edit-title" style="display: none;">
<a class="btn" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a>
</div>
</li>
</ul>
<div class="row-fluid"> <div class="row-fluid">
<div class="span10"> <div class="span10">
@commentlist(issue, comments, hasWritePermission, repository) @commentlist(issue, comments, hasWritePermission, repository)

View File

@@ -6,7 +6,7 @@
@import view.helpers._ @import view.helpers._
@html.main(s"Labels - ${repository.owner}/${repository.name}"){ @html.main(s"Labels - ${repository.owner}/${repository.name}"){
@html.menu("issues", repository){ @html.menu("issues", repository){
@issues.html.tab("labels", hasWritePermission, repository) @issues.html.navigation("labels", hasWritePermission, repository)
<br> <br>
<table class="table table-bordered table-hover table-issues" id="new-label-table" style="display: none;"> <table class="table table-bordered table-hover table-issues" id="new-label-table" style="display: none;">
<tr><td></td></tr> <tr><td></td></tr>

View File

@@ -13,7 +13,7 @@
@import view.helpers._ @import view.helpers._
@html.main((if(target == "issues") "Issues" else "Pull requests") + s" - ${repository.owner}/${repository.name}", Some(repository)){ @html.main((if(target == "issues") "Issues" else "Pull requests") + s" - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu(target, 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) @listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
@if(hasWritePermission){ @if(hasWritePermission){
<form id="batcheditForm" method="POST"> <form id="batcheditForm" method="POST">

View File

@@ -7,7 +7,7 @@
<h4>New milestone</h4> <h4>New milestone</h4>
<div class="muted">Create a new milestone to help organize your issues and pull requests.</div> <div class="muted">Create a new milestone to help organize your issues and pull requests.</div>
} else { } else {
@issues.html.tab("milestones", false, repository) @issues.html.navigation("milestones", false, repository)
<br><br> <br><br>
} }
<hr style="margin-top: 12px; margin-bottom: 18px;" class="fill-width"/> <hr style="margin-top: 12px; margin-bottom: 18px;" class="fill-width"/>

View File

@@ -6,7 +6,7 @@
@import view.helpers._ @import view.helpers._
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){ @html.main(s"Milestones - ${repository.owner}/${repository.name}"){
@html.menu("issues", repository){ @html.menu("issues", repository){
@issues.html.tab("milestones", hasWritePermission, repository) @issues.html.navigation("milestones", hasWritePermission, repository)
<br> <br>
<table class="table table-bordered table-hover table-issues"> <table class="table table-bordered table-hover table-issues">
<tr> <tr>

View File

@@ -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._
<ul class="nav nav-pills-group pull-left fill-width">
<li class="@if(active == "issues" ){active} first"><a href="@url(repository)/issues">Issues</a></li>
<li class="@if(active == "pulls" ){active}"><a href="@url(repository)/pulls">Pull requests</a></li>
<li class="@if(active == "labels" ){active}"><a href="@url(repository)/issues/labels">Labels</a></li>
<li class="@if(active == "milestones"){active} last"><a href="@url(repository)/issues/milestones">Milestones</a></li>
<li class="pull-right">
<form method="GET" id="search-filter-form" style="margin-bottom: 0px;">
@condition.map { condition =>
@if(loginAccount.isDefined){
<div class="input-prepend" style="margin-bottom: 0px;">
<div class="btn-group">
<button type="button" class="btn dropdown-toggle" data-toggle="dropdown" style="height: 34px;">
Filter
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a href="?q=is:open">Open issues and pull requests</a></li>
<li><a href="?q=is:open+is:issue+author:@urlEncode(loginAccount.get.userName)">Your issues</a></li>
<li><a href="?q=is:open+is:pr+author:@urlEncode(loginAccount.get.userName)">Your pull requests</a></li>
<li><a href="?q=is:open+assignee:@urlEncode(loginAccount.get.userName)">Everything assigned to you</a></li>
@*
<li><a href="?q=is:open+mentions:@urlEncode(loginAccount.get.userName)">Everything mentioning you</a></li>
*@
</ul>
</div>
<input type="text" id="search-filter-box" class="input-xlarge" name="q" style="height: 24px;" value="is:@{if(active == "issues") "issue" else "pr"} @condition.toFilterString"/>
</div>
} else {
<input type="text" id="search-filter-box" class="input-xlarge" name="q" style="height: 24px;" value="is:@{if(active == "issues") "issue" else "pr"} @condition.toFilterString"/>
}
}
@if(loginAccount.isDefined){
<div class="btn-group">
@if(newButton){
@if(active == "issues"){
<a class="btn btn-success" href="@url(repository)/issues/new" style="height: 24px;">New issue</a>
}
@if(active == "pulls"){
<a class="btn btn-success" href="@url(repository)/compare" style="height: 24px;">New pull request</a>
}
@if(active == "labels"){
<a class="btn btn-success" href="javascript:void(0);" id="new-label-button" style="height: 24px;">New label</a>
}
@if(active == "milestones"){
<a class="btn btn-success" href="@url(repository)/issues/milestones/new" style="height: 24px;">New milestone</a>
}
}
</div>
}
</form>
</li>
</ul>

View File

@@ -1,30 +0,0 @@
@(active: String, newButton: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
<ul class="nav nav-pills-group pull-left fill-width">
<li class="@if(active == "issues" ){active} first"><a href="@url(repository)/issues">Issues</a></li>
<li class="@if(active == "pulls" ){active}"><a href="@url(repository)/pulls">Pull requests</a></li>
<li class="@if(active == "labels" ){active}"><a href="@url(repository)/issues/labels">Labels</a></li>
<li class="@if(active == "milestones"){active} last"><a href="@url(repository)/issues/milestones">Milestones</a></li>
@if(loginAccount.isDefined){
<li class="pull-right">
<div class="btn-group">
@if(newButton){
@if(active == "issues"){
<a class="btn btn-success" href="@url(repository)/issues/new">New issue</a>
}
@if(active == "pulls"){
<a class="btn btn-success" href="@url(repository)/compare">New pull request</a>
}
@if(active == "labels"){
<a class="btn btn-success" href="javascript:void(0);" id="new-label-button">New label</a>
}
@if(active == "milestones"){
<a class="btn btn-success" href="@url(repository)/issues/milestones/new">New milestone</a>
}
}
</div>
</li>
}
</ul>

View File

@@ -45,7 +45,7 @@
</div> </div>
@if(commits.nonEmpty && hasWritePermission){ @if(commits.nonEmpty && hasWritePermission){
<div style="margin-bottom: 10px;" id="create-pull-request"> <div style="margin-bottom: 10px;" id="create-pull-request">
<a href="#" class="btn" id="show-form">Click to create a pull request for this comparison</a> <a href="#" class="btn btn-success" id="show-form">Create pull request</a>
</div> </div>
<div id="pull-request-form" class="box" style="display: none;"> <div id="pull-request-form" class="box" style="display: none;">
<div class="box-content"> <div class="box-content">

View File

@@ -50,22 +50,9 @@
</div> </div>
} }
@issues.html.commentform(issue, !merged, hasWritePermission, repository) @issues.html.commentform(issue, !merged, hasWritePermission, repository)
}
</div> </div>
<div class="span2"> <div class="span2">
@if(issue.closed) {
@if(merged){
<span class="label label-info issue-status">Merged</span>
} else {
<span class="label label-important issue-status">Closed</span>
}
} else {
<span class="label label-success issue-status">Open</span>
}
<div class="small" style="text-align: center;">
<span class="strong">@comments.size</span> @plural(comments.size, "comment")
</div>
}
<hr/>
@issues.html.issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository) @issues.html.issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
</div> </div>
</div> </div>

View File

@@ -6,4 +6,4 @@
<h4 style="color: #468847;">Able to merge</h4> <h4 style="color: #468847;">Able to merge</h4>
<p>These branches can be automatically merged.</p> <p>These branches can be automatically merged.</p>
} }
<input type="submit" class="btn btn-success btn-block" value="Send pull request"/> <input type="submit" class="btn btn-success btn-block" value="Create pull request"/>

View File

@@ -36,7 +36,7 @@
</p> </p>
} }
@helper.html.copy("repository-url-copy", requestRepositoryUrl){ @helper.html.copy("repository-url-copy", requestRepositoryUrl){
<input type="text" value="@requestRepositoryUrl" id="repository-url" readonly> <input type="text" style="width: 500px;" value="@requestRepositoryUrl" id="repository-url" readonly>
} }
<div> <div>
<p> <p>

View File

@@ -14,24 +14,50 @@
@html.main(s"${issue.title} - Pull Request #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){ @html.main(s"${issue.title} - Pull Request #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("pulls", repository){ @html.menu("pulls", repository){
@defining(dayByDayCommits.flatten){ commits => @defining(dayByDayCommits.flatten){ commits =>
<div class="pullreq-info"> <div>
<div class="show-title pull-right">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a class="btn btn-small" href="#" id="edit">Edit</a>
}
<a class="btn btn-small btn-success" href="@url(repository)/issues/new">New issue</a>
</div>
<div class="edit-title pull-right" style="display: none;">
<a class="btn" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a>
</div>
<h1>
<span class="show-title">
<span id="show-title">@issue.title</span>
<span class="muted">#@issue.issueId</span>
</span>
<span class="edit-title" style="display: none;">
<span id="error-edit-title" class="error"></span>
<input type="text" style="width: 700px;" id="edit-title" value="@issue.title"/>
</span>
</h1>
</div>
@if(issue.closed) { @if(issue.closed) {
@comments.find(_.action == "merge").map{ comment => @comments.find(_.action == "merge").map{ comment =>
<span class="label label-info">Merged</span> <span class="label label-info issue-status">Merged</span>
<span class="muted">
@user(comment.commentedUserName, styleClass="username strong") merged @commits.size @plural(commits.size, "commit") @user(comment.commentedUserName, styleClass="username strong") merged @commits.size @plural(commits.size, "commit")
into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code> into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
@helper.html.datetimeago(comment.registeredDate) @helper.html.datetimeago(comment.registeredDate)
</span>
}.getOrElse { }.getOrElse {
<span class="label label-important">Closed</span> <span class="label label-important issue-status">Closed</span>
<span class="muted">
@user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit") @user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit")
into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code> into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
</span>
} }
} else { } else {
<span class="label label-success">Open</span> <span class="label label-success issue-status">Open</span>
<span class="muted">
@user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit") @user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit")
into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code> into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
</span>
} }
</div> <br/><br/>
<ul class="nav nav-tabs fill-width pull-left" id="pullreq-tab"> <ul class="nav nav-tabs fill-width pull-left" id="pullreq-tab">
<li class="active"><a href="#conversation">Conversation <span class="badge">@comments.size</span></a></li> <li class="active"><a href="#conversation">Conversation <span class="badge">@comments.size</span></a></li>
<li><a href="#commits">Commits <span class="badge">@commits.size</span></a></li> <li><a href="#commits">Commits <span class="badge">@commits.size</span></a></li>
@@ -52,8 +78,41 @@
} }
} }
<script> <script>
$('#pullreq-tab a').click(function (e) { $(function(){
$('#pullreq-tab a').click(function (e) {
e.preventDefault(); e.preventDefault();
$(this).tab('show'); $(this).tab('show');
});
$('#edit').click(function(){
$('.edit-title').show();
$('.show-title').hide();
return false;
});
$('#update').click(function(){
$(this).attr('disabled', 'disabled');
$.ajax({
url: '@url(repository)/issues/edit_title/@issue.issueId',
type: 'POST',
data: {
title : $('#edit-title').val()
}
}).done(function(data){
$('#show-title').empty().text(data.title);
$('#cancel').click();
$(this).removeAttr('disabled');
}).fail(function(req){
$(this).removeAttr('disabled');
$('#error-edit-title').text($.parseJSON(req.responseText).title);
});
return false;
});
$('#cancel').click(function(){
$('.edit-title').hide();
$('.show-title').show();
return false;
});
}); });
</script> </script>

View File

@@ -10,12 +10,12 @@
<h1 class="wiki-title"><span class="muted">Editing</span> @if(pageName.isEmpty){New Page} else {@pageName}</h1> <h1 class="wiki-title"><span class="muted">Editing</span> @if(pageName.isEmpty){New Page} else {@pageName}</h1>
</li> </li>
<li class="pull-right"> <li class="pull-right">
<div class="btn-group"> <div>
@if(page.isDefined){ @if(page.isDefined){
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)">View Page</a>
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_delete" id="delete">Delete Page</a> <a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_delete" id="delete">Delete Page</a>
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_history">Page History</a> <a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_history">Page History</a>
} }
<a class="btn btn-small btn-success" href="@url(repository)/wiki/_new">New Page</a>
</div> </div>
</li> </li>
</ul> </ul>

View File

@@ -16,21 +16,29 @@
</h1> </h1>
</li> </li>
<li class="pull-right"> <li class="pull-right">
<div class="btn-group"> <div>
@if(pageName.isEmpty){ @if(pageName.isEmpty){
@if(loginAccount.isDefined){ @if(loginAccount.isDefined){
<a class="btn btn-small" href="@url(repository)/wiki/_new">New Page</a> <a class="btn btn-small" href="@url(repository)/wiki/_new">New Page</a>
} }
} else { } else {
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)">View Page</a>
@if(loginAccount.isDefined){ @if(loginAccount.isDefined){
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_edit">Edit Page</a> <a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_edit">Edit Page</a>
<a class="btn btn-small btn-success" href="@url(repository)/wiki/_new">New Page</a>
} }
} }
</div> </div>
</li> </li>
</ul> </ul>
<table class="table table-bordered fill-width pull-left"> <table class="table table-bordered fill-width pull-left">
<tr>
<th colspan="3">
<div class="pull-left" style="padding-top: 4px;">Revisions</div>
<div class="pull-right">
<input type="button" id="compare" value="Compare Revisions" class="btn btn-mini"/>
</div>
</th>
</tr>
@commits.map { commit => @commits.map { commit =>
<tr> <tr>
<td width="0%"><input type="checkbox" name="commitId" value="@commit.id"></td> <td width="0%"><input type="checkbox" name="commitId" value="@commit.id"></td>
@@ -41,8 +49,6 @@
</tr> </tr>
} }
</table> </table>
<input type="button" id="compare" value="Compare Revisions" class="btn"/>
<input type="button" id="top" value="Back to Top" class="btn"/>
<script> <script>
$(function(){ $(function(){
$('input[name=commitId]').click(function(){ $('input[name=commitId]').click(function(){

View File

@@ -16,13 +16,12 @@
</div> </div>
</li> </li>
<li class="pull-right"> <li class="pull-right">
<div class="btn-group">
@if(hasWritePermission){ @if(hasWritePermission){
<a class="btn btn-small" href="@url(repository)/wiki/_new">New Page</a> <div>
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_edit">Edit Page</a> <a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_edit">Edit Page</a>
} <a class="btn btn-small btn-success" href="@url(repository)/wiki/_new">New Page</a>
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_history">Page History</a>
</div> </div>
}
</li> </li>
</ul> </ul>
<div style="width: 200px;" class="pull-right"> <div style="width: 200px;" class="pull-right">

View File

@@ -93,6 +93,7 @@ class AvatarImageProviderSpec extends Specification with Mockito {
private def createSystemSettings(useGravatar: Boolean) = private def createSystemSettings(useGravatar: Boolean) =
SystemSettings( SystemSettings(
baseUrl = None, baseUrl = None,
information = None,
allowAccountRegistration = false, allowAccountRegistration = false,
gravatar = useGravatar, gravatar = useGravatar,
notification = false, notification = false,