diff --git a/README.md b/README.md
index 4ef29df..43470be 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@
- File editing in repository viewer
- Comment for the changeset
- Network graph
-- Statics
+- Statistics
- Watch / Star
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki).
@@ -42,7 +42,6 @@
- --port=[NUMBER]
- --prefix=[CONTEXTPATH]
- --host=[HOSTNAME]
-- --https=true
- --gitbucket.home=[DATA_DIR]
To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/.gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk.
@@ -59,12 +58,24 @@
Release Notes
--------
+### 1.11 - 01 Mar 2014
+- Base URL for redirection, notification and repository URL box is configurable
+- Remove ```--https``` option because it's possible to substitute in the base url
+- Headline anchor is available for Markdown contents such as Wiki page
+- Improve H2 connectivity
+- Label is available for pull requests not only issues
+- Delete branch button is added
+- Repository icons are updated
+- Select lines of source code by URL hash like `#L10` or `#L10-L15` in repository viewer
+- Display reference to issue from others in comment list
+- Fix some bugs
+
### 1.10 - 01 Feb 2014
- Rename repository
- Transfer repository owner
- Change default data directory to `HOME/.gitbucket` from `HOME/gitbucket` to avoid problem like #243, but if data directory already exist at HOME/gitbucket, it continues being used.
- Add LDAP display name attribute
-- Response improvement
+- Response performance improvement
- Fix some bugs
### 1.9 - 28 Dec 2013
diff --git a/contrib/redhat/gitbucket.conf b/contrib/redhat/gitbucket.conf
index c3959f3..103778e 100644
--- a/contrib/redhat/gitbucket.conf
+++ b/contrib/redhat/gitbucket.conf
@@ -4,9 +4,6 @@
# Server port
#GITBUCKET_PORT=8080
-# Force HTTPS scheme
-#GITBUCKET_HTTPS=false
-
# Data directory (GITBUCKET_HOME/gitbucket)
#GITBUCKET_HOME=/var/lib/gitbucket
diff --git a/contrib/redhat/gitbucket.init b/contrib/redhat/gitbucket.init
index 3aed802..43e29e3 100644
--- a/contrib/redhat/gitbucket.init
+++ b/contrib/redhat/gitbucket.init
@@ -39,9 +39,6 @@
if [ $GITBUCKET_HOST ]; then
START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
fi
- if [ $GITBUCKET_HTTPS ]; then
- START_OPTS="${START_OPTS} --https=true"
- fi
# Run the Java process
GITBUCKET_HOME="${GITBUCKET_HOME}" java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 &
diff --git a/etc/icons.svg b/etc/icons.svg
index 6943304..c2efe0e 100644
--- a/etc/icons.svg
+++ b/etc/icons.svg
@@ -25,17 +25,17 @@
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.4"
- inkscape:cx="629.30023"
- inkscape:cy="281.44758"
+ inkscape:cx="450.21999"
+ inkscape:cy="97.51519"
inkscape:document-units="px"
inkscape:current-layer="layer1-9"
showgrid="false"
inkscape:window-width="1366"
- inkscape:window-height="705"
- inkscape:window-x="-8"
+ inkscape:window-height="706"
+ inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
- inkscape:snap-global="true"
+ inkscape:snap-global="false"
inkscape:snap-grids="false"
inkscape:snap-page="false"
inkscape:snap-bbox="true"
@@ -746,6 +746,238 @@
d="m 937.41093,1044.4944 0,30.6797 -28.50183,0 0,41.2377 28.50183,0 0,27.1288 41.19033,0 0,-27.1288 29.35404,0 0,-41.2377 -29.35404,0 0,-30.6797 -41.19033,0 z"
id="rect2995-0-2-7-7"
inkscape:connector-curvature="0" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/JettyLauncher.java b/src/main/java/JettyLauncher.java
index d7b137d..76500f0 100644
--- a/src/main/java/JettyLauncher.java
+++ b/src/main/java/JettyLauncher.java
@@ -25,8 +25,6 @@
port = Integer.parseInt(dim[1]);
} else if(dim[0].equals("--prefix")) {
contextPath = dim[1];
- } else if(dim[0].equals("--https") && (dim[1].equals("1") || dim[1].equals("true"))) {
- forceHttps = true;
} else if(dim[0].equals("--gitbucket.home")){
System.setProperty("gitbucket.home", dim[1]);
}
@@ -36,7 +34,7 @@
Server server = new Server();
- HttpsSupportConnector connector = new HttpsSupportConnector(forceHttps);
+ SelectChannelConnector connector = new SelectChannelConnector();
if(host != null) {
connector.setHost(host);
}
@@ -62,19 +60,3 @@
server.join();
}
}
-
-class HttpsSupportConnector extends SelectChannelConnector {
- private boolean forceHttps;
-
- public HttpsSupportConnector(boolean forceHttps) {
- this.forceHttps = forceHttps;
- }
-
- @Override
- public void customize(final EndPoint endpoint, final Request request) throws IOException {
- if (this.forceHttps) {
- request.setScheme("https");
- super.customize(endpoint, request);
- }
- }
-}
diff --git a/src/main/scala/app/AccountController.scala b/src/main/scala/app/AccountController.scala
index b32d5df..df24aad 100644
--- a/src/main/scala/app/AccountController.scala
+++ b/src/main/scala/app/AccountController.scala
@@ -5,16 +5,13 @@
import util.StringUtil._
import util.Directory._
import jp.sf.amateras.scalatra.forms._
-import org.scalatra.FlashMapSupport
import org.apache.commons.io.FileUtils
class AccountController extends AccountControllerBase
- with SystemSettingsService with AccountService with RepositoryService with ActivityService
- with OneselfAuthenticator
+ with AccountService with RepositoryService with ActivityService with OneselfAuthenticator
-trait AccountControllerBase extends AccountManagementControllerBase with FlashMapSupport {
- self: SystemSettingsService with AccountService with RepositoryService with ActivityService
- with OneselfAuthenticator =>
+trait AccountControllerBase extends AccountManagementControllerBase {
+ self: AccountService with RepositoryService with ActivityService with OneselfAuthenticator =>
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String])
diff --git a/src/main/scala/app/ControllerBase.scala b/src/main/scala/app/ControllerBase.scala
index a026a18..5e60eb4 100644
--- a/src/main/scala/app/ControllerBase.scala
+++ b/src/main/scala/app/ControllerBase.scala
@@ -10,8 +10,7 @@
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import model.Account
-import scala.Some
-import service.AccountService
+import service.{SystemSettingsService, AccountService}
import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest}
import java.text.SimpleDateFormat
import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
@@ -21,7 +20,8 @@
* Provides generic features for controller implementations.
*/
abstract class ControllerBase extends ScalatraFilter
- with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with Validations {
+ with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations
+ with SystemSettingsService {
implicit val jsonFormats = DefaultFormats
@@ -58,11 +58,7 @@
/**
* Returns the context object for the request.
*/
- implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL, request)
-
- private def currentURL: String = defining(request.getQueryString){ queryString =>
- request.getRequestURI + (if(queryString != null) "?" + queryString else "")
- }
+ implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, request)
private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount)
@@ -107,27 +103,27 @@
if(request.getMethod.toUpperCase == "POST"){
org.scalatra.Unauthorized(redirect("/signin"))
} else {
- org.scalatra.Unauthorized(redirect("/signin?redirect=" + StringUtil.urlEncode(currentURL)))
+ org.scalatra.Unauthorized(redirect("/signin?redirect=" + StringUtil.urlEncode(
+ defining(request.getQueryString){ queryString =>
+ request.getRequestURI.substring(request.getContextPath.length) + (if(queryString != null) "?" + queryString else "")
+ }
+ )))
}
}
}
- protected def baseUrl = defining(request.getRequestURL.toString){ url =>
- url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
- }
+ override def fullUrl(path: String, params: Iterable[(String, Any)] = Iterable.empty,
+ includeContextPath: Boolean = true, includeServletPath: Boolean = true)
+ (implicit request: HttpServletRequest, response: HttpServletResponse) =
+ if (path.startsWith("http")) path
+ else baseUrl + url(path, params, false, false)
}
/**
* Context object for the current request.
*/
-case class Context(path: String, loginAccount: Option[Account], currentUrl: String, request: HttpServletRequest){
-
- def redirectUrl = if(request.getParameter("redirect") != null){
- request.getParameter("redirect")
- } else {
- currentUrl
- }
+case class Context(path: String, loginAccount: Option[Account], request: HttpServletRequest){
/**
* Get object from cache.
diff --git a/src/main/scala/app/FileUploadController.scala b/src/main/scala/app/FileUploadController.scala
index 6ea5fa2..9950b48 100644
--- a/src/main/scala/app/FileUploadController.scala
+++ b/src/main/scala/app/FileUploadController.scala
@@ -12,8 +12,7 @@
* This servlet saves uploaded file as temporary file and returns the unique id.
* You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id.
*/
-class FileUploadController extends ScalatraServlet
- with FileUploadSupport with FlashMapSupport with FileUploadControllerBase {
+class FileUploadController extends ScalatraServlet with FileUploadSupport with FileUploadControllerBase {
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
diff --git a/src/main/scala/app/IndexController.scala b/src/main/scala/app/IndexController.scala
index a71c1a1..3ffb57a 100644
--- a/src/main/scala/app/IndexController.scala
+++ b/src/main/scala/app/IndexController.scala
@@ -1,88 +1,85 @@
-package app
-
-import util._
-import util.Implicits._
-import service._
-import jp.sf.amateras.scalatra.forms._
-
-class IndexController extends IndexControllerBase
- with RepositoryService with SystemSettingsService with ActivityService with AccountService
-with UsersAuthenticator
-
-trait IndexControllerBase extends ControllerBase {
- self: RepositoryService with SystemSettingsService with ActivityService with AccountService with UsersAuthenticator =>
-
- case class SignInForm(userName: String, password: String)
-
- val form = mapping(
- "userName" -> trim(label("Username", text(required))),
- "password" -> trim(label("Password", text(required)))
- )(SignInForm.apply)
-
- get("/"){
- val loginAccount = context.loginAccount
-
- html.index(getRecentActivities(),
- getVisibleRepositories(loginAccount, baseUrl),
- loadSystemSettings(),
- loginAccount.map{ account => getUserRepositories(account.userName, baseUrl) }.getOrElse(Nil)
- )
- }
-
- get("/signin"){
- val redirect = params.get("redirect")
- if(redirect.isDefined && redirect.get.startsWith("/")){
- session.setAttribute(Keys.Session.Redirect, redirect.get)
- }
- html.signin(loadSystemSettings())
- }
-
- post("/signin", form){ form =>
- authenticate(loadSystemSettings(), form.userName, form.password) match {
- case Some(account) => signin(account)
- case None => redirect("/signin")
- }
- }
-
- get("/signout"){
- session.invalidate
- redirect("/")
- }
-
- /**
- * Set account information into HttpSession and redirect.
- */
- private def signin(account: model.Account) = {
- session.setAttribute(Keys.Session.LoginAccount, account)
- updateLastLoginDate(account.userName)
-
- if(AccountUtil.hasLdapDummyMailAddress(account)) {
- session.remove(Keys.Session.Redirect)
- redirect("/" + account.userName + "/_edit")
- }
-
- session.getAndRemove[String](Keys.Session.Redirect).map { redirectUrl =>
- if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){
- redirect("/")
- } else {
- redirect(redirectUrl)
- }
- }.getOrElse {
- redirect("/")
- }
- }
-
- /**
- * JSON API for collaborator completion.
- *
- * TODO Move to other controller?
- */
- get("/_user/proposals")(usersOnly {
- contentType = formats("json")
- org.json4s.jackson.Serialization.write(
- Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray)
- )
- })
-
-
-}
+package app
+
+import util._
+import service._
+import jp.sf.amateras.scalatra.forms._
+
+class IndexController extends IndexControllerBase
+ with RepositoryService with ActivityService with AccountService with UsersAuthenticator
+
+trait IndexControllerBase extends ControllerBase {
+ self: RepositoryService with ActivityService with AccountService with UsersAuthenticator =>
+
+ case class SignInForm(userName: String, password: String)
+
+ val form = mapping(
+ "userName" -> trim(label("Username", text(required))),
+ "password" -> trim(label("Password", text(required)))
+ )(SignInForm.apply)
+
+ get("/"){
+ val loginAccount = context.loginAccount
+
+ html.index(getRecentActivities(),
+ getVisibleRepositories(loginAccount, baseUrl),
+ loadSystemSettings(),
+ loginAccount.map{ account => getUserRepositories(account.userName, baseUrl) }.getOrElse(Nil)
+ )
+ }
+
+ get("/signin"){
+ val redirect = params.get("redirect")
+ if(redirect.isDefined && redirect.get.startsWith("/")){
+ flash += Keys.Flash.Redirect -> redirect.get
+ }
+ html.signin(loadSystemSettings())
+ }
+
+ post("/signin", form){ form =>
+ authenticate(loadSystemSettings(), form.userName, form.password) match {
+ case Some(account) => signin(account)
+ case None => redirect("/signin")
+ }
+ }
+
+ get("/signout"){
+ session.invalidate
+ redirect("/")
+ }
+
+ /**
+ * Set account information into HttpSession and redirect.
+ */
+ private def signin(account: model.Account) = {
+ session.setAttribute(Keys.Session.LoginAccount, account)
+ updateLastLoginDate(account.userName)
+
+ if(AccountUtil.hasLdapDummyMailAddress(account)) {
+ redirect("/" + account.userName + "/_edit")
+ }
+
+ flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl =>
+ if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){
+ redirect("/")
+ } else {
+ redirect(redirectUrl)
+ }
+ }.getOrElse {
+ redirect("/")
+ }
+ }
+
+ /**
+ * JSON API for collaborator completion.
+ *
+ * TODO Move to other controller?
+ */
+ get("/_user/proposals")(usersOnly {
+ contentType = formats("json")
+ org.json4s.jackson.Serialization.write(
+ Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray)
+ )
+ })
+
+
+}
diff --git a/src/main/scala/app/IssuesController.scala b/src/main/scala/app/IssuesController.scala
index 22544e7..be564ee 100644
--- a/src/main/scala/app/IssuesController.scala
+++ b/src/main/scala/app/IssuesController.scala
@@ -4,10 +4,11 @@
import service._
import IssuesService._
-import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator, Notifier, Keys}
+import util._
import util.Implicits._
import util.ControlUtil._
import org.scalatra.Ok
+import model.Issue
class IssuesController extends IssuesControllerBase
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
@@ -110,6 +111,11 @@
// record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
+ // extract references and create refer comment
+ getIssue(owner, name, issueId.toString).foreach { issue =>
+ createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
+ }
+
// notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgIssue(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
@@ -123,7 +129,11 @@
defining(repository.owner, repository.name){ case (owner, name) =>
getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){
+ // update issue
updateIssue(owner, name, issue.issueId, form.title, form.content)
+ // extract references and create refer comment
+ createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
+
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized
} getOrElse NotFound
@@ -274,6 +284,15 @@
redirect(s"/${repository.owner}/${repository.name}/issues")
}
+ private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
+ StringUtil.extractIssueId(message).foreach { issueId =>
+ if(getIssue(owner, repository, issueId).isDefined){
+ createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt,
+ fromIssue.issueId + ":" + fromIssue.title, "refer")
+ }
+ }
+ }
+
/**
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
*/
@@ -313,6 +332,11 @@
}
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
+ // extract references and create refer comment
+ content.map { content =>
+ createReferComment(owner, name, issue, content)
+ }
+
// notifications
Notifier() match {
case f =>
diff --git a/src/main/scala/app/PullRequestsController.scala b/src/main/scala/app/PullRequestsController.scala
index 1f8453c..bf3bea2 100644
--- a/src/main/scala/app/PullRequestsController.scala
+++ b/src/main/scala/app/PullRequestsController.scala
@@ -79,7 +79,7 @@
pulls.html.pullreq(
issue, pullreq,
getComments(owner, name, issueId),
- getIssueLabels(owner, name, issueId.toInt),
+ getIssueLabels(owner, name, issueId),
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestonesWithIssueCount(owner, name),
getLabels(owner, name),
@@ -183,6 +183,18 @@
}
}
+ // close issue by content of pull request
+ val defaultBranch = getRepository(owner, name, baseUrl).get.repository.defaultBranch
+ if(pullreq.branch == defaultBranch){
+ commits.flatten.foreach { commit =>
+ closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
+ }
+ issue.content match {
+ case Some(content) => closeIssuesFromMessage(content, loginAccount.userName, owner, name)
+ case _ =>
+ }
+ closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
+ }
// call web hook
getWebHookURLs(owner, name) match {
case webHookURLs if(webHookURLs.nonEmpty) =>
diff --git a/src/main/scala/app/RepositorySettingsController.scala b/src/main/scala/app/RepositorySettingsController.scala
index d745623..3f5489a 100644
--- a/src/main/scala/app/RepositorySettingsController.scala
+++ b/src/main/scala/app/RepositorySettingsController.scala
@@ -5,7 +5,6 @@
import util.{UsersAuthenticator, OwnerAuthenticator}
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
-import org.scalatra.FlashMapSupport
import org.scalatra.i18n.Messages
import service.WebHookService.WebHookPayload
import util.JGitUtil.CommitInfo
@@ -16,7 +15,7 @@
with RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator
-trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSupport {
+trait RepositorySettingsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator =>
diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala
index 2726136..fcc81df 100644
--- a/src/main/scala/app/RepositoryViewerController.scala
+++ b/src/main/scala/app/RepositoryViewerController.scala
@@ -276,7 +276,7 @@
val readme = files.find { file =>
readmeFiles.contains(file.name.toLowerCase)
}.map { file =>
- StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
+ file -> StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
}
repo.html.files(revision, repository,
diff --git a/src/main/scala/app/SearchController.scala b/src/main/scala/app/SearchController.scala
index 99ea743..ab091cf 100644
--- a/src/main/scala/app/SearchController.scala
+++ b/src/main/scala/app/SearchController.scala
@@ -6,13 +6,10 @@
import jp.sf.amateras.scalatra.forms._
class SearchController extends SearchControllerBase
-with RepositoryService with AccountService with SystemSettingsService with ActivityService
-with RepositorySearchService with IssuesService
-with ReferrerAuthenticator
+ with RepositoryService with AccountService with ActivityService with RepositorySearchService with IssuesService with ReferrerAuthenticator
trait SearchControllerBase extends ControllerBase { self: RepositoryService
- with SystemSettingsService with ActivityService with RepositorySearchService
- with ReferrerAuthenticator =>
+ with ActivityService with RepositorySearchService with ReferrerAuthenticator =>
val searchForm = mapping(
"query" -> trim(text(required)),
diff --git a/src/main/scala/app/SystemSettingsController.scala b/src/main/scala/app/SystemSettingsController.scala
index b9707c0..d19c146 100644
--- a/src/main/scala/app/SystemSettingsController.scala
+++ b/src/main/scala/app/SystemSettingsController.scala
@@ -4,15 +4,15 @@
import SystemSettingsService._
import util.AdminAuthenticator
import jp.sf.amateras.scalatra.forms._
-import org.scalatra.FlashMapSupport
class SystemSettingsController extends SystemSettingsControllerBase
with SystemSettingsService with AccountService with AdminAuthenticator
-trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
+trait SystemSettingsControllerBase extends ControllerBase {
self: SystemSettingsService with AccountService with AdminAuthenticator =>
private val form = mapping(
+ "baseUrl" -> trim(label("Base URL", 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/WikiController.scala b/src/main/scala/app/WikiController.scala
index 704ce20..07208a5 100644
--- a/src/main/scala/app/WikiController.scala
+++ b/src/main/scala/app/WikiController.scala
@@ -6,18 +6,15 @@
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.api.Git
-import org.scalatra.FlashMapSupport
import org.scalatra.i18n.Messages
import scala.Some
import java.util.ResourceBundle
class WikiController extends WikiControllerBase
- with WikiService with RepositoryService with AccountService with ActivityService
- with CollaboratorsAuthenticator with ReferrerAuthenticator
+ with WikiService with RepositoryService with AccountService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator
-trait WikiControllerBase extends ControllerBase with FlashMapSupport {
- self: WikiService with RepositoryService with ActivityService
- with CollaboratorsAuthenticator with ReferrerAuthenticator =>
+trait WikiControllerBase extends ControllerBase {
+ self: WikiService with RepositoryService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator =>
case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String)
diff --git a/src/main/scala/service/AccountService.scala b/src/main/scala/service/AccountService.scala
index 54a705e..d00c4cd 100644
--- a/src/main/scala/service/AccountService.scala
+++ b/src/main/scala/service/AccountService.scala
@@ -65,7 +65,7 @@
Query(Accounts) filter(t => (t.userName is userName.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false): Option[Account] =
- Query(Accounts) filter(t => (t.mailAddress is mailAddress.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
+ Query(Accounts) filter(t => (t.mailAddress.toLowerCase is mailAddress.toLowerCase.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
def getAllUsers(includeRemoved: Boolean = true): List[Account] =
if(includeRemoved){
diff --git a/src/main/scala/service/IssuesService.scala b/src/main/scala/service/IssuesService.scala
index 54edaf3..b121599 100644
--- a/src/main/scala/service/IssuesService.scala
+++ b/src/main/scala/service/IssuesService.scala
@@ -8,6 +8,7 @@
import model._
import util.Implicits._
import util.StringUtil._
+import util.StringUtil
trait IssuesService {
import IssuesService._
@@ -314,6 +315,14 @@
}.toList
}
+ def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String) = {
+ StringUtil.extractCloseId(message).foreach { issueId =>
+ for(issue <- getIssue(owner, repository, issueId) if !issue.closed){
+ createComment(owner, repository, userName, issue.issueId, "Close", "close")
+ updateClosed(owner, repository, issue.issueId, true)
+ }
+ }
+ }
}
object IssuesService {
diff --git a/src/main/scala/service/SystemSettingsService.scala b/src/main/scala/service/SystemSettingsService.scala
index 8d1e82c..9c5d300 100644
--- a/src/main/scala/service/SystemSettingsService.scala
+++ b/src/main/scala/service/SystemSettingsService.scala
@@ -1,172 +1,183 @@
-package service
-
-import util.Directory._
-import util.ControlUtil._
-import SystemSettingsService._
-
-trait SystemSettingsService {
-
- def saveSystemSettings(settings: SystemSettings): Unit = {
- defining(new java.util.Properties()){ props =>
- props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
- props.setProperty(Gravatar, settings.gravatar.toString)
- props.setProperty(Notification, settings.notification.toString)
- if(settings.notification) {
- settings.smtp.foreach { smtp =>
- props.setProperty(SmtpHost, smtp.host)
- smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString))
- smtp.user.foreach(props.setProperty(SmtpUser, _))
- smtp.password.foreach(props.setProperty(SmtpPassword, _))
- smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString))
- smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _))
- smtp.fromName.foreach(props.setProperty(SmtpFromName, _))
- }
- }
- props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString)
- if(settings.ldapAuthentication){
- settings.ldap.map { ldap =>
- props.setProperty(LdapHost, ldap.host)
- ldap.port.foreach(x => props.setProperty(LdapPort, x.toString))
- ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x))
- ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
- props.setProperty(LdapBaseDN, ldap.baseDN)
- props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
- ldap.additionalFilterCondition.foreach(x => props.setProperty(LdapAdditionalFilterCondition, x))
- ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
- props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute)
- ldap.disableMailResolve.foreach(x => props.setProperty(LdapDisableMailResolve, x.toString))
- ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
- ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
- }
- }
- props.store(new java.io.FileOutputStream(GitBucketConf), null)
- }
- }
-
-
- def loadSystemSettings(): SystemSettings = {
- defining(new java.util.Properties()){ props =>
- if(GitBucketConf.exists){
- props.load(new java.io.FileInputStream(GitBucketConf))
- }
- SystemSettings(
- getValue(props, AllowAccountRegistration, false),
- getValue(props, Gravatar, true),
- getValue(props, Notification, false),
- if(getValue(props, Notification, false)){
- Some(Smtp(
- getValue(props, SmtpHost, ""),
- getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
- getOptionValue(props, SmtpUser, None),
- getOptionValue(props, SmtpPassword, None),
- getOptionValue[Boolean](props, SmtpSsl, None),
- getOptionValue(props, SmtpFromAddress, None),
- getOptionValue(props, SmtpFromName, None)))
- } else {
- None
- },
- getValue(props, LdapAuthentication, false),
- if(getValue(props, LdapAuthentication, false)){
- Some(Ldap(
- getValue(props, LdapHost, ""),
- getOptionValue(props, LdapPort, Some(DefaultLdapPort)),
- getOptionValue(props, LdapBindDN, None),
- getOptionValue(props, LdapBindPassword, None),
- getValue(props, LdapBaseDN, ""),
- getValue(props, LdapUserNameAttribute, ""),
- getOptionValue(props, LdapAdditionalFilterCondition, None),
- getOptionValue(props, LdapFullNameAttribute, None),
- getValue(props, LdapMailAddressAttribute, ""),
- getOptionValue[Boolean](props, LdapDisableMailResolve, None),
- getOptionValue[Boolean](props, LdapTls, None),
- getOptionValue(props, LdapKeystore, None)))
- } else {
- None
- }
- )
- }
- }
-
-}
-
-object SystemSettingsService {
- import scala.reflect.ClassTag
-
- case class SystemSettings(
- allowAccountRegistration: Boolean,
- gravatar: Boolean,
- notification: Boolean,
- smtp: Option[Smtp],
- ldapAuthentication: Boolean,
- ldap: Option[Ldap])
-
- case class Ldap(
- host: String,
- port: Option[Int],
- bindDN: Option[String],
- bindPassword: Option[String],
- baseDN: String,
- userNameAttribute: String,
- additionalFilterCondition: Option[String],
- fullNameAttribute: Option[String],
- mailAttribute: String,
- disableMailResolve: Option[Boolean],
- tls: Option[Boolean],
- keystore: Option[String])
-
- case class Smtp(
- host: String,
- port: Option[Int],
- user: Option[String],
- password: Option[String],
- ssl: Option[Boolean],
- fromAddress: Option[String],
- fromName: Option[String])
-
- val DefaultSmtpPort = 25
- val DefaultLdapPort = 389
-
- private val AllowAccountRegistration = "allow_account_registration"
- private val Gravatar = "gravatar"
- private val Notification = "notification"
- private val SmtpHost = "smtp.host"
- private val SmtpPort = "smtp.port"
- private val SmtpUser = "smtp.user"
- private val SmtpPassword = "smtp.password"
- private val SmtpSsl = "smtp.ssl"
- private val SmtpFromAddress = "smtp.from_address"
- private val SmtpFromName = "smtp.from_name"
- private val LdapAuthentication = "ldap_authentication"
- private val LdapHost = "ldap.host"
- private val LdapPort = "ldap.port"
- private val LdapBindDN = "ldap.bindDN"
- private val LdapBindPassword = "ldap.bind_password"
- private val LdapBaseDN = "ldap.baseDN"
- private val LdapUserNameAttribute = "ldap.username_attribute"
- private val LdapAdditionalFilterCondition = "ldap.additional_filter_condition"
- private val LdapFullNameAttribute = "ldap.fullname_attribute"
- private val LdapMailAddressAttribute = "ldap.mail_attribute"
- private val LdapDisableMailResolve = "ldap.disable_mail_resolve"
- private val LdapTls = "ldap.tls"
- private val LdapKeystore = "ldap.keystore"
-
- private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
- defining(props.getProperty(key)){ value =>
- if(value == null || value.isEmpty) default
- else convertType(value).asInstanceOf[A]
- }
-
- private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] =
- defining(props.getProperty(key)){ value =>
- if(value == null || value.isEmpty) default
- else Some(convertType(value)).asInstanceOf[Option[A]]
- }
-
- private def convertType[A: ClassTag](value: String) =
- defining(implicitly[ClassTag[A]].runtimeClass){ c =>
- if(c == classOf[Boolean]) value.toBoolean
- else if(c == classOf[Int]) value.toInt
- else value
- }
-
-}
+package service
+
+import util.Directory._
+import util.ControlUtil._
+import SystemSettingsService._
+import javax.servlet.http.HttpServletRequest
+
+trait SystemSettingsService {
+
+ def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings().baseUrl.getOrElse {
+ defining(request.getRequestURL.toString){ url =>
+ url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
+ }
+ }.replaceFirst("/$", "")
+
+ def saveSystemSettings(settings: SystemSettings): Unit = {
+ defining(new java.util.Properties()){ props =>
+ settings.baseUrl.foreach(props.setProperty(BaseURL, _))
+ props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
+ props.setProperty(Gravatar, settings.gravatar.toString)
+ props.setProperty(Notification, settings.notification.toString)
+ if(settings.notification) {
+ settings.smtp.foreach { smtp =>
+ props.setProperty(SmtpHost, smtp.host)
+ smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString))
+ smtp.user.foreach(props.setProperty(SmtpUser, _))
+ smtp.password.foreach(props.setProperty(SmtpPassword, _))
+ smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString))
+ smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _))
+ smtp.fromName.foreach(props.setProperty(SmtpFromName, _))
+ }
+ }
+ props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString)
+ if(settings.ldapAuthentication){
+ settings.ldap.map { ldap =>
+ props.setProperty(LdapHost, ldap.host)
+ ldap.port.foreach(x => props.setProperty(LdapPort, x.toString))
+ ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x))
+ ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
+ props.setProperty(LdapBaseDN, ldap.baseDN)
+ props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
+ ldap.additionalFilterCondition.foreach(x => props.setProperty(LdapAdditionalFilterCondition, x))
+ ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
+ props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute)
+ ldap.disableMailResolve.foreach(x => props.setProperty(LdapDisableMailResolve, x.toString))
+ ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
+ ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
+ }
+ }
+ props.store(new java.io.FileOutputStream(GitBucketConf), null)
+ }
+ }
+
+
+ def loadSystemSettings(): SystemSettings = {
+ defining(new java.util.Properties()){ props =>
+ if(GitBucketConf.exists){
+ props.load(new java.io.FileInputStream(GitBucketConf))
+ }
+ SystemSettings(
+ getOptionValue(props, BaseURL, None),
+ getValue(props, AllowAccountRegistration, false),
+ getValue(props, Gravatar, true),
+ getValue(props, Notification, false),
+ if(getValue(props, Notification, false)){
+ Some(Smtp(
+ getValue(props, SmtpHost, ""),
+ getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
+ getOptionValue(props, SmtpUser, None),
+ getOptionValue(props, SmtpPassword, None),
+ getOptionValue[Boolean](props, SmtpSsl, None),
+ getOptionValue(props, SmtpFromAddress, None),
+ getOptionValue(props, SmtpFromName, None)))
+ } else {
+ None
+ },
+ getValue(props, LdapAuthentication, false),
+ if(getValue(props, LdapAuthentication, false)){
+ Some(Ldap(
+ getValue(props, LdapHost, ""),
+ getOptionValue(props, LdapPort, Some(DefaultLdapPort)),
+ getOptionValue(props, LdapBindDN, None),
+ getOptionValue(props, LdapBindPassword, None),
+ getValue(props, LdapBaseDN, ""),
+ getValue(props, LdapUserNameAttribute, ""),
+ getOptionValue(props, LdapAdditionalFilterCondition, None),
+ getOptionValue(props, LdapFullNameAttribute, None),
+ getValue(props, LdapMailAddressAttribute, ""),
+ getOptionValue[Boolean](props, LdapDisableMailResolve, None),
+ getOptionValue[Boolean](props, LdapTls, None),
+ getOptionValue(props, LdapKeystore, None)))
+ } else {
+ None
+ }
+ )
+ }
+ }
+
+}
+
+object SystemSettingsService {
+ import scala.reflect.ClassTag
+
+ case class SystemSettings(
+ baseUrl: Option[String],
+ allowAccountRegistration: Boolean,
+ gravatar: Boolean,
+ notification: Boolean,
+ smtp: Option[Smtp],
+ ldapAuthentication: Boolean,
+ ldap: Option[Ldap])
+
+ case class Ldap(
+ host: String,
+ port: Option[Int],
+ bindDN: Option[String],
+ bindPassword: Option[String],
+ baseDN: String,
+ userNameAttribute: String,
+ additionalFilterCondition: Option[String],
+ fullNameAttribute: Option[String],
+ mailAttribute: String,
+ disableMailResolve: Option[Boolean],
+ tls: Option[Boolean],
+ keystore: Option[String])
+
+ case class Smtp(
+ host: String,
+ port: Option[Int],
+ user: Option[String],
+ password: Option[String],
+ ssl: Option[Boolean],
+ fromAddress: Option[String],
+ fromName: Option[String])
+
+ val DefaultSmtpPort = 25
+ val DefaultLdapPort = 389
+
+ private val BaseURL = "base_url"
+ private val AllowAccountRegistration = "allow_account_registration"
+ private val Gravatar = "gravatar"
+ private val Notification = "notification"
+ private val SmtpHost = "smtp.host"
+ private val SmtpPort = "smtp.port"
+ private val SmtpUser = "smtp.user"
+ private val SmtpPassword = "smtp.password"
+ private val SmtpSsl = "smtp.ssl"
+ private val SmtpFromAddress = "smtp.from_address"
+ private val SmtpFromName = "smtp.from_name"
+ private val LdapAuthentication = "ldap_authentication"
+ private val LdapHost = "ldap.host"
+ private val LdapPort = "ldap.port"
+ private val LdapBindDN = "ldap.bindDN"
+ private val LdapBindPassword = "ldap.bind_password"
+ private val LdapBaseDN = "ldap.baseDN"
+ private val LdapUserNameAttribute = "ldap.username_attribute"
+ private val LdapAdditionalFilterCondition = "ldap.additional_filter_condition"
+ private val LdapFullNameAttribute = "ldap.fullname_attribute"
+ private val LdapMailAddressAttribute = "ldap.mail_attribute"
+ private val LdapDisableMailResolve = "ldap.disable_mail_resolve"
+ private val LdapTls = "ldap.tls"
+ private val LdapKeystore = "ldap.keystore"
+
+ private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
+ defining(props.getProperty(key)){ value =>
+ if(value == null || value.isEmpty) default
+ else convertType(value).asInstanceOf[A]
+ }
+
+ private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] =
+ defining(props.getProperty(key)){ value =>
+ if(value == null || value.isEmpty) default
+ else Some(convertType(value)).asInstanceOf[Option[A]]
+ }
+
+ private def convertType[A: ClassTag](value: String) =
+ defining(implicitly[ClassTag[A]].runtimeClass){ c =>
+ if(c == classOf[Boolean]) value.toBoolean
+ else if(c == classOf[Int]) value.toInt
+ else value
+ }
+
+}
diff --git a/src/main/scala/servlet/AutoUpdateListener.scala b/src/main/scala/servlet/AutoUpdateListener.scala
index aa07876..bfa6992 100644
--- a/src/main/scala/servlet/AutoUpdateListener.scala
+++ b/src/main/scala/servlet/AutoUpdateListener.scala
@@ -50,6 +50,7 @@
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
+ Version(1, 11),
Version(1, 10),
Version(1, 9),
Version(1, 8),
diff --git a/src/main/scala/servlet/GitRepositoryServlet.scala b/src/main/scala/servlet/GitRepositoryServlet.scala
index 712cd81..9efefd3 100644
--- a/src/main/scala/servlet/GitRepositoryServlet.scala
+++ b/src/main/scala/servlet/GitRepositoryServlet.scala
@@ -9,7 +9,7 @@
import javax.servlet.ServletConfig
import javax.servlet.ServletContext
import javax.servlet.http.HttpServletRequest
-import util.{Keys, JGitUtil, Directory}
+import util.{StringUtil, Keys, JGitUtil, Directory}
import util.ControlUtil._
import util.Implicits._
import service._
@@ -50,10 +50,10 @@
}
-class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] {
+class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory])
-
+
override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
val receivePack = new ReceivePack(db)
val pusher = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
@@ -64,13 +64,11 @@
defining(request.paths){ paths =>
val owner = paths(1)
val repository = paths(2).replaceFirst("\\.git$", "")
- val baseURL = request.getRequestURL.toString.replaceFirst("/git/.*", "")
logger.debug("repository:" + owner + "/" + repository)
- logger.debug("baseURL:" + baseURL)
if(!repository.endsWith(".wiki")){
- receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseURL))
+ receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseUrl(request)))
}
receivePack
}
@@ -79,7 +77,7 @@
import scala.collection.JavaConverters._
-class CommitLogHook(owner: String, repository: String, pusher: String, baseURL: String) extends PostReceiveHook
+class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String) extends PostReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
@@ -143,12 +141,20 @@
}
}
+ // close issues
+ val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch
+ if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){
+ git.log.addRange(command.getOldId, command.getNewId).call.asScala.foreach { commit =>
+ closeIssuesFromMessage(commit.getFullMessage, pusher, owner, repository)
+ }
+ }
+
// call web hook
getWebHookURLs(owner, repository) match {
case webHookURLs if(webHookURLs.nonEmpty) =>
for(pusherAccount <- getAccountByUserName(pusher);
ownerAccount <- getAccountByUserName(owner);
- repositoryInfo <- getRepository(owner, repository, baseURL)){
+ repositoryInfo <- getRepository(owner, repository, baseUrl)){
callWebHook(owner, repository, webHookURLs,
WebHookPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount))
}
@@ -167,8 +173,7 @@
}
private def createIssueComment(commit: CommitInfo) = {
- "(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData =>
- val issueId = matchData.group(2)
+ StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
if(getIssue(owner, repository, issueId).isDefined){
getAccountByMailAddress(commit.mailAddress).foreach { account =>
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
@@ -182,7 +187,7 @@
*/
private def updatePullRequests(branch: String) =
getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq =>
- if(getRepository(pullreq.userName, pullreq.repositoryName, baseURL).isDefined){
+ if(getRepository(pullreq.userName, pullreq.repositoryName, baseUrl).isDefined){
using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName))){ git =>
git.fetch
.setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString)
diff --git a/src/main/scala/util/ControlUtil.scala b/src/main/scala/util/ControlUtil.scala
index 02d7fd1..0b0f712 100644
--- a/src/main/scala/util/ControlUtil.scala
+++ b/src/main/scala/util/ControlUtil.scala
@@ -3,7 +3,7 @@
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.TreeWalk
-import org.eclipse.jgit.transport.RefSpec
+import scala.util.control.Exception._
import scala.language.reflectiveCalls
/**
@@ -16,10 +16,8 @@
def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B =
try f(resource) finally {
if(resource != null){
- try {
+ ignoring(classOf[Throwable]) {
resource.close()
- } catch {
- case e: Throwable => // ignore
}
}
}
diff --git a/src/main/scala/util/Implicits.scala b/src/main/scala/util/Implicits.scala
index a9e648c..77c095e 100644
--- a/src/main/scala/util/Implicits.scala
+++ b/src/main/scala/util/Implicits.scala
@@ -1,6 +1,7 @@
package util
import scala.util.matching.Regex
+import scala.util.control.Exception._
import javax.servlet.http.{HttpSession, HttpServletRequest}
/**
@@ -42,10 +43,8 @@
sb.toString
}
- def toIntOpt: Option[Int] = try {
- Option(Integer.parseInt(value))
- } catch {
- case e: NumberFormatException => None
+ def toIntOpt: Option[Int] = catching(classOf[NumberFormatException]) opt {
+ Integer.parseInt(value)
}
}
diff --git a/src/main/scala/util/JGitUtil.scala b/src/main/scala/util/JGitUtil.scala
index 742641e..57fd421 100644
--- a/src/main/scala/util/JGitUtil.scala
+++ b/src/main/scala/util/JGitUtil.scala
@@ -128,7 +128,7 @@
using(Git.open(getRepositoryDir(owner, repository))){ git =>
try {
// get commit count
- val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(1000).sum
+ val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10000).sum
RepositoryInfo(
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git",
diff --git a/src/main/scala/util/Keys.scala b/src/main/scala/util/Keys.scala
index 4aabe35..920dfca 100644
--- a/src/main/scala/util/Keys.scala
+++ b/src/main/scala/util/Keys.scala
@@ -13,12 +13,7 @@
/**
* Session key for the logged in account information.
*/
- val LoginAccount = "LOGIN_ACCOUNT"
-
- /**
- * Session key for the redirect URL.
- */
- val Redirect = "REDIRECT"
+ val LoginAccount = "loginAccount"
/**
* Session key for the issue search condition in dashboard.
@@ -47,6 +42,20 @@
}
+ object Flash {
+
+ /**
+ * Flash key for the redirect URL.
+ */
+ val Redirect = "redirect"
+
+ /**
+ * Flash key for the information message.
+ */
+ val Info = "info"
+
+ }
+
/**
* Define request keys.
*/
diff --git a/src/main/scala/util/StringUtil.scala b/src/main/scala/util/StringUtil.scala
index 55c923a..54da029 100644
--- a/src/main/scala/util/StringUtil.scala
+++ b/src/main/scala/util/StringUtil.scala
@@ -31,7 +31,7 @@
/**
* Make string from byte array. Character encoding is detected automatically by [[util.StringUtil.detectEncoding]].
- * And if given bytes contains UTF-8 BOM, it's removed from returned string..
+ * And if given bytes contains UTF-8 BOM, it's removed from returned string.
*/
def convertFromByteArray(content: Array[Byte]): String =
IOUtils.toString(new BOMInputStream(new java.io.ByteArrayInputStream(content)), detectEncoding(content))
@@ -45,4 +45,23 @@
case e => e
}
}
+
+ /**
+ * Extract issue id like ```#issueId``` from the given message.
+ *
+ *@param message the message which may contains issue id
+ * @return the iterator of issue id
+ */
+ def extractIssueId(message: String): Iterator[String] =
+ "(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(message).matchData.map(_.group(2))
+
+ /**
+ * Extract close issue id like ```close #issueId ``` from the given message.
+ *
+ * @param message the message which may contains close command
+ * @return the iterator of issue id
+ */
+ def extractCloseId(message: String): Iterator[String] =
+ "(?i)(?")
- printer.print(s"""""")
+ printer.print(s"""<$tag class="markdown-head">""")
+ printer.print(s"""""")
+ printer.print(s"""""")
visitChildren(node)
printer.print(s"$tag>")
}
@@ -142,12 +143,10 @@
private val Whitespace = "[\\s]".r
- private val SpecialChars = "[^\\w-]".r
-
def generateAnchorName(text: String): String = {
val noWhitespace = Whitespace.replaceAllIn(text, "-")
val normalized = Normalizer.normalize(noWhitespace, Normalizer.Form.NFD)
- val noSpecialChars = SpecialChars.replaceAllIn(normalized, "")
+ val noSpecialChars = StringUtil.urlEncode(normalized)
noSpecialChars.toLowerCase(Locale.ENGLISH)
}
}
diff --git a/src/main/twirl/admin/system.scala.html b/src/main/twirl/admin/system.scala.html
index 74a1486..dbafeb4 100644
--- a/src/main/twirl/admin/system.scala.html
+++ b/src/main/twirl/admin/system.scala.html
@@ -3,205 +3,220 @@
@import util.Directory._
@import view.helpers._
@html.main("System Settings"){
-@menu("system"){
-@helper.html.information(info)
-