Merge branch 'master' into asciidoctorj

This commit is contained in:
Tobias Roeser
2014-03-02 09:15:57 +01:00
24 changed files with 124 additions and 85 deletions

View File

@@ -42,7 +42,6 @@ or you can start GitBucket by `java -jar gitbucket.war` without servlet containe
- --port=[NUMBER] - --port=[NUMBER]
- --prefix=[CONTEXTPATH] - --prefix=[CONTEXTPATH]
- --host=[HOSTNAME] - --host=[HOSTNAME]
- --https=true
- --gitbucket.home=[DATA_DIR] - --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. 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,8 +58,9 @@ Run the following commands in `Terminal` to
Release Notes Release Notes
-------- --------
### 1.11 - End of Feb 2014 ### 1.11 - 01 Mar 2014
- Base URL for redirect, notification and repository URL box is configurable - 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 - Headline anchor is available for Markdown contents such as Wiki page
- Improve H2 connectivity - Improve H2 connectivity
- Label is available for pull requests not only issues - Label is available for pull requests not only issues

View File

@@ -4,9 +4,6 @@
# Server port # Server port
#GITBUCKET_PORT=8080 #GITBUCKET_PORT=8080
# Force HTTPS scheme
#GITBUCKET_HTTPS=false
# Data directory (GITBUCKET_HOME/gitbucket) # Data directory (GITBUCKET_HOME/gitbucket)
#GITBUCKET_HOME=/var/lib/gitbucket #GITBUCKET_HOME=/var/lib/gitbucket

View File

@@ -39,9 +39,6 @@ start() {
if [ $GITBUCKET_HOST ]; then if [ $GITBUCKET_HOST ]; then
START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}" START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
fi fi
if [ $GITBUCKET_HTTPS ]; then
START_OPTS="${START_OPTS} --https=true"
fi
# Run the Java process # Run the Java process
GITBUCKET_HOME="${GITBUCKET_HOME}" java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 & GITBUCKET_HOME="${GITBUCKET_HOME}" java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 &

View File

@@ -25,8 +25,6 @@ public class JettyLauncher {
port = Integer.parseInt(dim[1]); port = Integer.parseInt(dim[1]);
} else if(dim[0].equals("--prefix")) { } else if(dim[0].equals("--prefix")) {
contextPath = dim[1]; 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")){ } else if(dim[0].equals("--gitbucket.home")){
System.setProperty("gitbucket.home", dim[1]); System.setProperty("gitbucket.home", dim[1]);
} }
@@ -36,7 +34,7 @@ public class JettyLauncher {
Server server = new Server(); Server server = new Server();
HttpsSupportConnector connector = new HttpsSupportConnector(forceHttps); SelectChannelConnector connector = new SelectChannelConnector();
if(host != null) { if(host != null) {
connector.setHost(host); connector.setHost(host);
} }
@@ -62,19 +60,3 @@ public class JettyLauncher {
server.join(); 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);
}
}
}

View File

@@ -5,13 +5,12 @@ import util.{FileUtil, OneselfAuthenticator}
import util.StringUtil._ import util.StringUtil._
import util.Directory._ import util.Directory._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.scalatra.FlashMapSupport
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
class AccountController extends AccountControllerBase class AccountController extends AccountControllerBase
with AccountService with RepositoryService with ActivityService with OneselfAuthenticator with AccountService with RepositoryService with ActivityService with OneselfAuthenticator
trait AccountControllerBase extends AccountManagementControllerBase with FlashMapSupport { trait AccountControllerBase extends AccountManagementControllerBase {
self: AccountService with RepositoryService with ActivityService with OneselfAuthenticator => self: AccountService with RepositoryService with ActivityService with OneselfAuthenticator =>
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String, case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,

View File

@@ -20,7 +20,8 @@ import org.scalatra.i18n._
* Provides generic features for controller implementations. * Provides generic features for controller implementations.
*/ */
abstract class ControllerBase extends ScalatraFilter abstract class ControllerBase extends ScalatraFilter
with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with Validations with SystemSettingsService { with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations
with SystemSettingsService {
implicit val jsonFormats = DefaultFormats implicit val jsonFormats = DefaultFormats
@@ -102,20 +103,20 @@ abstract class ControllerBase extends ScalatraFilter
if(request.getMethod.toUpperCase == "POST"){ if(request.getMethod.toUpperCase == "POST"){
org.scalatra.Unauthorized(redirect("/signin")) org.scalatra.Unauthorized(redirect("/signin"))
} else { } else {
val currentUrl = baseUrl + defining(request.getQueryString){ queryString => org.scalatra.Unauthorized(redirect("/signin?redirect=" + StringUtil.urlEncode(
request.getRequestURI.substring(request.getContextPath.length) + (if(queryString != null) "?" + queryString else "") defining(request.getQueryString){ queryString =>
} request.getRequestURI.substring(request.getContextPath.length) + (if(queryString != null) "?" + queryString else "")
session.setAttribute(Keys.Session.Redirect, currentUrl) }
org.scalatra.Unauthorized(redirect("/signin")) )))
} }
} }
} }
protected def baseUrl = loadSystemSettings().baseUrl.getOrElse { override def fullUrl(path: String, params: Iterable[(String, Any)] = Iterable.empty,
defining(request.getRequestURL.toString){ url => includeContextPath: Boolean = true, includeServletPath: Boolean = true)
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length)) (implicit request: HttpServletRequest, response: HttpServletResponse) =
} if (path.startsWith("http")) path
}.replaceFirst("/$", "") else baseUrl + url(path, params, includeContextPath, includeServletPath)
} }

View File

@@ -12,8 +12,7 @@ import org.apache.commons.io.FileUtils
* This servlet saves uploaded file as temporary file and returns the unique id. * 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. * You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id.
*/ */
class FileUploadController extends ScalatraServlet class FileUploadController extends ScalatraServlet with FileUploadSupport with FileUploadControllerBase {
with FileUploadSupport with FlashMapSupport with FileUploadControllerBase {
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024))) configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))

View File

@@ -1,7 +1,6 @@
package app package app
import util._ import util._
import util.Implicits._
import service._ import service._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
@@ -31,7 +30,7 @@ trait IndexControllerBase extends ControllerBase {
get("/signin"){ get("/signin"){
val redirect = params.get("redirect") val redirect = params.get("redirect")
if(redirect.isDefined && redirect.get.startsWith("/")){ if(redirect.isDefined && redirect.get.startsWith("/")){
session.setAttribute(Keys.Session.Redirect, redirect.get) flash += Keys.Flash.Redirect -> redirect.get
} }
html.signin(loadSystemSettings()) html.signin(loadSystemSettings())
} }
@@ -55,7 +54,7 @@ trait IndexControllerBase extends ControllerBase {
session.setAttribute(Keys.Session.LoginAccount, account) session.setAttribute(Keys.Session.LoginAccount, account)
updateLastLoginDate(account.userName) updateLastLoginDate(account.userName)
session.getAndRemove[String](Keys.Session.Redirect).map { redirectUrl => flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl =>
if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){ if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){
redirect("/") redirect("/")
} else { } else {

View File

@@ -79,7 +79,7 @@ trait PullRequestsControllerBase extends ControllerBase {
pulls.html.pullreq( pulls.html.pullreq(
issue, pullreq, issue, pullreq,
getComments(owner, name, issueId), 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, (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestonesWithIssueCount(owner, name), getMilestonesWithIssueCount(owner, name),
getLabels(owner, name), getLabels(owner, name),
@@ -183,6 +183,18 @@ trait PullRequestsControllerBase extends ControllerBase {
} }
} }
// 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 // call web hook
getWebHookURLs(owner, name) match { getWebHookURLs(owner, name) match {
case webHookURLs if(webHookURLs.nonEmpty) => case webHookURLs if(webHookURLs.nonEmpty) =>

View File

@@ -5,7 +5,6 @@ import util.Directory._
import util.{UsersAuthenticator, OwnerAuthenticator} import util.{UsersAuthenticator, OwnerAuthenticator}
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.scalatra.FlashMapSupport
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import service.WebHookService.WebHookPayload import service.WebHookService.WebHookPayload
import util.JGitUtil.CommitInfo import util.JGitUtil.CommitInfo
@@ -16,7 +15,7 @@ class RepositorySettingsController extends RepositorySettingsControllerBase
with RepositoryService with AccountService with WebHookService with RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator with OwnerAuthenticator with UsersAuthenticator
trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSupport { trait RepositorySettingsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with WebHookService self: RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator => with OwnerAuthenticator with UsersAuthenticator =>

View File

@@ -4,12 +4,11 @@ import service.{AccountService, SystemSettingsService}
import SystemSettingsService._ import SystemSettingsService._
import util.AdminAuthenticator import util.AdminAuthenticator
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.scalatra.FlashMapSupport
class SystemSettingsController extends SystemSettingsControllerBase class SystemSettingsController extends SystemSettingsControllerBase
with SystemSettingsService with AccountService with AdminAuthenticator with SystemSettingsService with AccountService with AdminAuthenticator
trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport { trait SystemSettingsControllerBase extends ControllerBase {
self: SystemSettingsService with AccountService with AdminAuthenticator => self: SystemSettingsService with AccountService with AdminAuthenticator =>
private val form = mapping( private val form = mapping(

View File

@@ -6,18 +6,15 @@ import util.Directory._
import util.ControlUtil._ import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.scalatra.FlashMapSupport
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import scala.Some import scala.Some
import java.util.ResourceBundle import java.util.ResourceBundle
class WikiController extends WikiControllerBase class WikiController extends WikiControllerBase
with WikiService with RepositoryService with AccountService with ActivityService with WikiService with RepositoryService with AccountService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator
with CollaboratorsAuthenticator with ReferrerAuthenticator
trait WikiControllerBase extends ControllerBase with FlashMapSupport { trait WikiControllerBase extends ControllerBase {
self: WikiService with RepositoryService with ActivityService self: WikiService with RepositoryService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator =>
with CollaboratorsAuthenticator with ReferrerAuthenticator =>
case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String) case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String)

View File

@@ -59,7 +59,7 @@ trait AccountService {
Query(Accounts) filter(t => (t.userName is userName.bind) && (t.removed is false.bind, !includeRemoved)) firstOption 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] = 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] = def getAllUsers(includeRemoved: Boolean = true): List[Account] =
if(includeRemoved){ if(includeRemoved){

View File

@@ -8,6 +8,7 @@ import Q.interpolation
import model._ import model._
import util.Implicits._ import util.Implicits._
import util.StringUtil._ import util.StringUtil._
import util.StringUtil
trait IssuesService { trait IssuesService {
import IssuesService._ import IssuesService._
@@ -314,6 +315,14 @@ trait IssuesService {
}.toList }.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 { object IssuesService {

View File

@@ -3,9 +3,16 @@ package service
import util.Directory._ import util.Directory._
import util.ControlUtil._ import util.ControlUtil._
import SystemSettingsService._ import SystemSettingsService._
import javax.servlet.http.HttpServletRequest
trait SystemSettingsService { 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 = { def saveSystemSettings(settings: SystemSettings): Unit = {
defining(new java.util.Properties()){ props => defining(new java.util.Properties()){ props =>
settings.baseUrl.foreach(props.setProperty(BaseURL, _)) settings.baseUrl.foreach(props.setProperty(BaseURL, _))

View File

@@ -50,7 +50,7 @@ class GitRepositoryServlet extends GitServlet {
} }
class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] { class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory]) private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory])
@@ -64,13 +64,11 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
defining(request.paths){ paths => defining(request.paths){ paths =>
val owner = paths(1) val owner = paths(1)
val repository = paths(2).replaceFirst("\\.git$", "") val repository = paths(2).replaceFirst("\\.git$", "")
val baseURL = request.getRequestURL.toString.replaceFirst("/git/.*", "")
logger.debug("repository:" + owner + "/" + repository) logger.debug("repository:" + owner + "/" + repository)
logger.debug("baseURL:" + baseURL)
if(!repository.endsWith(".wiki")){ if(!repository.endsWith(".wiki")){
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseURL)) receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseUrl(request)))
} }
receivePack receivePack
} }
@@ -79,7 +77,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
import scala.collection.JavaConverters._ 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 { with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook]) private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
@@ -143,12 +141,20 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseURL:
} }
} }
// 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 // call web hook
getWebHookURLs(owner, repository) match { getWebHookURLs(owner, repository) match {
case webHookURLs if(webHookURLs.nonEmpty) => case webHookURLs if(webHookURLs.nonEmpty) =>
for(pusherAccount <- getAccountByUserName(pusher); for(pusherAccount <- getAccountByUserName(pusher);
ownerAccount <- getAccountByUserName(owner); ownerAccount <- getAccountByUserName(owner);
repositoryInfo <- getRepository(owner, repository, baseURL)){ repositoryInfo <- getRepository(owner, repository, baseUrl)){
callWebHook(owner, repository, webHookURLs, callWebHook(owner, repository, webHookURLs,
WebHookPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount)) WebHookPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount))
} }
@@ -181,7 +187,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseURL:
*/ */
private def updatePullRequests(branch: String) = private def updatePullRequests(branch: String) =
getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq => 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 => using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName))){ git =>
git.fetch git.fetch
.setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString) .setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString)

View File

@@ -3,7 +3,7 @@ package util
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevWalk import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.TreeWalk import org.eclipse.jgit.treewalk.TreeWalk
import org.eclipse.jgit.transport.RefSpec import scala.util.control.Exception._
import scala.language.reflectiveCalls import scala.language.reflectiveCalls
/** /**
@@ -16,10 +16,8 @@ object ControlUtil {
def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B = def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B =
try f(resource) finally { try f(resource) finally {
if(resource != null){ if(resource != null){
try { ignoring(classOf[Throwable]) {
resource.close() resource.close()
} catch {
case e: Throwable => // ignore
} }
} }
} }

View File

@@ -1,6 +1,7 @@
package util package util
import scala.util.matching.Regex import scala.util.matching.Regex
import scala.util.control.Exception._
import javax.servlet.http.{HttpSession, HttpServletRequest} import javax.servlet.http.{HttpSession, HttpServletRequest}
/** /**
@@ -42,10 +43,8 @@ object Implicits {
sb.toString sb.toString
} }
def toIntOpt: Option[Int] = try { def toIntOpt: Option[Int] = catching(classOf[NumberFormatException]) opt {
Option(Integer.parseInt(value)) Integer.parseInt(value)
} catch {
case e: NumberFormatException => None
} }
} }

View File

@@ -128,7 +128,7 @@ object JGitUtil {
using(Git.open(getRepositoryDir(owner, repository))){ git => using(Git.open(getRepositoryDir(owner, repository))){ git =>
try { try {
// get commit count // 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( RepositoryInfo(
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", owner, repository, s"${baseUrl}/git/${owner}/${repository}.git",

View File

@@ -13,12 +13,7 @@ object Keys {
/** /**
* Session key for the logged in account information. * Session key for the logged in account information.
*/ */
val LoginAccount = "LOGIN_ACCOUNT" val LoginAccount = "loginAccount"
/**
* Session key for the redirect URL.
*/
val Redirect = "REDIRECT"
/** /**
* Session key for the issue search condition in dashboard. * Session key for the issue search condition in dashboard.
@@ -47,6 +42,20 @@ object Keys {
} }
object Flash {
/**
* Flash key for the redirect URL.
*/
val Redirect = "redirect"
/**
* Flash key for the information message.
*/
val Info = "info"
}
/** /**
* Define request keys. * Define request keys.
*/ */

View File

@@ -31,7 +31,7 @@ object StringUtil {
/** /**
* Make string from byte array. Character encoding is detected automatically by [[util.StringUtil.detectEncoding]]. * 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 = def convertFromByteArray(content: Array[Byte]): String =
IOUtils.toString(new BOMInputStream(new java.io.ByteArrayInputStream(content)), detectEncoding(content)) IOUtils.toString(new BOMInputStream(new java.io.ByteArrayInputStream(content)), detectEncoding(content))
@@ -47,12 +47,21 @@ object StringUtil {
} }
/** /**
* Extract issue id like ````#issueId``` from the given message. * Extract issue id like ```#issueId``` from the given message.
* *
*@param message the message which may contains issue id *@param message the message which may contains issue id
* @return the iterator of issue id * @return the iterator of issue id
*/ */
def extractIssueId(message: String): Iterator[String] = def extractIssueId(message: String): Iterator[String] =
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(message).matchData.map { matchData => matchData.group(2) } "(^|\\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)(?<!\\w)(?:fix(?:e[sd])?|resolve[sd]?|close[sd]?)\\s+#(\\d+)(?!\\w)".r.findAllIn(message).matchData.map(_.group(1))
} }

View File

@@ -30,6 +30,9 @@
} }
} }
</div> </div>
@repository.repository.description.map { description =>
<p>@description</p>
}
<table class="global-nav box-header"> <table class="global-nav box-header">
<tr> <tr>
<th class="box-header@if(active=="code"){ active}"> <th class="box-header@if(active=="code"){ active}">

View File

@@ -12,7 +12,7 @@
<div class="head"> <div class="head">
<div class="pull-right"> <div class="pull-right">
@defining(repository.commitCount){ commitCount => @defining(repository.commitCount){ commitCount =>
<a href="@url(repository)/commits/@encodeRefName(branch)">@if(commitCount > 1000){ @commitCount+ } else { @commitCount } @plural(commitCount, "commit")</a>&nbsp; <a href="@url(repository)/commits/@encodeRefName(branch)">@if(commitCount > 10000){ @commitCount+ } else { @commitCount } @plural(commitCount, "commit")</a>&nbsp;
} }
</div> </div>
<a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> / <a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> /

View File

@@ -35,4 +35,22 @@ class StringUtilSpec extends Specification {
StringUtil.sha1("abc") mustEqual "a9993e364706816aba3e25717850c26c9cd0d89d" StringUtil.sha1("abc") mustEqual "a9993e364706816aba3e25717850c26c9cd0d89d"
} }
} }
"extractIssueId" should {
"extract '#xxx' and return extracted id" in {
StringUtil.extractIssueId("(refs #123)").toSeq mustEqual Seq("123")
}
"returns Nil from message which does not contain #xxx" in {
StringUtil.extractIssueId("this is test!").toSeq mustEqual Nil
}
}
"extractCloseId" should {
"extract 'close #xxx' and return extracted id" in {
StringUtil.extractCloseId("(close #123)").toSeq mustEqual Seq("123")
}
"returns Nil from message which does not contain close command" in {
StringUtil.extractCloseId("(refs #123)").toSeq mustEqual Nil
}
}
} }