diff --git a/README.md b/README.md index fa6718d..41c57d1 100644 --- a/README.md +++ b/README.md @@ -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,8 +58,9 @@ Release Notes -------- -### 1.11 - End of Feb 2014 -- Base URL for redirect, notification and repository URL box is configurable +### 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 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/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 5bea0b4..df24aad 100644 --- a/src/main/scala/app/AccountController.scala +++ b/src/main/scala/app/AccountController.scala @@ -5,13 +5,12 @@ 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 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 => case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String, diff --git a/src/main/scala/app/ControllerBase.scala b/src/main/scala/app/ControllerBase.scala index bdea042..cf91fea 100644 --- a/src/main/scala/app/ControllerBase.scala +++ b/src/main/scala/app/ControllerBase.scala @@ -20,7 +20,8 @@ * Provides generic features for controller implementations. */ 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 @@ -102,20 +103,20 @@ if(request.getMethod.toUpperCase == "POST"){ org.scalatra.Unauthorized(redirect("/signin")) } else { - val currentUrl = baseUrl + 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")) + 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 = loadSystemSettings().baseUrl.getOrElse { - defining(request.getRequestURL.toString){ url => - url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length)) - } - }.replaceFirst("/$", "") + 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, includeContextPath, includeServletPath) } 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 2ee96bd..0e46b3c 100644 --- a/src/main/scala/app/IndexController.scala +++ b/src/main/scala/app/IndexController.scala @@ -1,7 +1,6 @@ package app import util._ -import util.Implicits._ import service._ import jp.sf.amateras.scalatra.forms._ @@ -31,7 +30,7 @@ get("/signin"){ val redirect = params.get("redirect") if(redirect.isDefined && redirect.get.startsWith("/")){ - session.setAttribute(Keys.Session.Redirect, redirect.get) + flash += Keys.Flash.Redirect -> redirect.get } html.signin(loadSystemSettings()) } @@ -55,7 +54,7 @@ session.setAttribute(Keys.Session.LoginAccount, account) 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){ redirect("/") } else { 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/SystemSettingsController.scala b/src/main/scala/app/SystemSettingsController.scala index 08231b6..c6f56fe 100644 --- a/src/main/scala/app/SystemSettingsController.scala +++ b/src/main/scala/app/SystemSettingsController.scala @@ -4,12 +4,11 @@ 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( 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 84e78a1..ff1186f 100644 --- a/src/main/scala/service/AccountService.scala +++ b/src/main/scala/service/AccountService.scala @@ -59,7 +59,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 de7a966..f15bd37 100644 --- a/src/main/scala/service/SystemSettingsService.scala +++ b/src/main/scala/service/SystemSettingsService.scala @@ -3,9 +3,16 @@ 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, _)) diff --git a/src/main/scala/servlet/GitRepositoryServlet.scala b/src/main/scala/servlet/GitRepositoryServlet.scala index 7eedd21..9efefd3 100644 --- a/src/main/scala/servlet/GitRepositoryServlet.scala +++ b/src/main/scala/servlet/GitRepositoryServlet.scala @@ -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)) } @@ -181,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 7de2a62..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)) @@ -47,12 +47,21 @@ } /** - * 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 * @return the iterator of issue id */ 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)(? +@repository.repository.description.map { description => +

@description

+}