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"") } @@ -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) -
-
+ @menu("system"){ + @helper.html.information(info) + +
System Settings
- - - - - @GitBucketHome - - - -
- -
- - -
- - - -
- -
- -
- - - -
- -
- -
-
-
- -
- - -
-
-
- -
- - -
-
-
- -
- - -
-
-
- -
- - -
-
-
- -
- - -
-
-
- -
- - -
-
-
- -
- - -
-
-
- -
- - -
-
-
- -
- - -
-
-
-
- -
-
-
-
- -
-
-
- -
- - -
-
+ + + + + @GitBucketHome + + + +
+ +
+
+ +
+
+

+ The base URL is used for redirect, notification email, git repository URL box and more. + If the base URL is empty, GitBucket generates URL from request information. + You can use this property to adjust URL difference between the reverse proxy and GitBucket. +

+ + + +
+ +
+ + +
+ + + +
+ +
+ +
+ + + +
+ +
+ +
+
+
+ +
+ + +
- - - -
- -
- -
-
-
- -
- - -
-
-
- -
- - -
-
-
- -
- -
-
-
- -
- -
-
-
-
- -
-
-
- -
- -
-
-
- -
- -
-
+
+ +
+ + +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+ +
+
+
+
+ +
+
+
+ +
+ + +
+
+
+ + + +
+ +
+ +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
-
-
+
+
-
- -} + + + } } \ No newline at end of file + diff --git a/src/main/twirl/header.scala.html b/src/main/twirl/header.scala.html index b132711..55f19ee 100644 --- a/src/main/twirl/header.scala.html +++ b/src/main/twirl/header.scala.html @@ -12,10 +12,13 @@ }
@if(repository.repository.isPrivate){ - - } - @if(!repository.repository.isPrivate){ - + + } else { + @if(repository.repository.originUserName.isDefined){ + + } else { + + } } @repository.owner / @repository.name @@ -27,6 +30,9 @@ } }
+@repository.repository.description.map { description => +

@description

+} diff --git a/src/main/twirl/issues/commentlist.scala.html b/src/main/twirl/issues/commentlist.scala.html index 7bc168f..92a82f7 100644 --- a/src/main/twirl/issues/commentlist.scala.html +++ b/src/main/twirl/issues/commentlist.scala.html @@ -11,10 +11,16 @@
- @user(comment.commentedUserName, styleClass="username strong") commented + @user(comment.commentedUserName, styleClass="username strong") + @if(comment.action == "comment"){ + commented + } else { + @if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request } + } @datetime(comment.registeredDate) - @if(comment.action != "commit" && comment.action != "merge" && (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){ + @if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" && + (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){   } @@ -27,7 +33,13 @@ @markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true) } } else { - @markdown(comment.content, repository, false, true) + @if(comment.action == "refer"){ + @defining(comment.content.split(":")){ case Array(issueId, rest @ _*) => + Issue #@issueId: @rest.mkString(":") + } + } else { + @markdown(comment.content, repository, false, true) + } }
diff --git a/src/main/twirl/main.scala.html b/src/main/twirl/main.scala.html index 592238a..7ec9842 100644 --- a/src/main/twirl/main.scala.html +++ b/src/main/twirl/main.scala.html @@ -61,7 +61,7 @@ } } else { - Sign in + Sign in } @@ -76,6 +76,7 @@ $('#search').submit(function(){ return $.trim($(this).find('input[name=query]').val()) != ''; }); + $('#signin').attr('href', '@path/signin?redirect=' + encodeURIComponent(location.pathname + location.search + location.hash)); }); diff --git a/src/main/twirl/newrepo.scala.html b/src/main/twirl/newrepo.scala.html index 6d24a9c..05fefbc 100644 --- a/src/main/twirl/newrepo.scala.html +++ b/src/main/twirl/newrepo.scala.html @@ -30,7 +30,7 @@
- @readme.map { content => + @readme.map { case(file, content) =>
-
README.md
+
@file.name
@markdown(content, repository, false, false)
} diff --git a/src/main/webapp/assets/common/css/gitbucket.css b/src/main/webapp/assets/common/css/gitbucket.css index 4c69e73..01c96b4 100644 --- a/src/main/webapp/assets/common/css/gitbucket.css +++ b/src/main/webapp/assets/common/css/gitbucket.css @@ -850,3 +850,21 @@ border-top-right-radius: 4px; -moz-border-radius-topright: 4px; } + +.markdown-head { + position: relative; +} + +a.markdown-anchor-link { + position: absolute; + left: -20px; + width: 32px; + height: 16px; + background-image: url(../images/link.png); + background-repeat: no-repeat; + display: none; +} + +h1 a.markdown-anchor-link, h2 a.markdown-anchor-link, h3 a.markdown-anchor-link { + top: 10px; +} diff --git a/src/main/webapp/assets/common/images/link.png b/src/main/webapp/assets/common/images/link.png new file mode 100644 index 0000000..25eacb7 --- /dev/null +++ b/src/main/webapp/assets/common/images/link.png Binary files differ diff --git a/src/main/webapp/assets/common/images/repo_fork.png b/src/main/webapp/assets/common/images/repo_fork.png new file mode 100644 index 0000000..576ec19 --- /dev/null +++ b/src/main/webapp/assets/common/images/repo_fork.png Binary files differ diff --git a/src/main/webapp/assets/common/images/repo_fork_lg.png b/src/main/webapp/assets/common/images/repo_fork_lg.png new file mode 100644 index 0000000..060a397 --- /dev/null +++ b/src/main/webapp/assets/common/images/repo_fork_lg.png Binary files differ diff --git a/src/main/webapp/assets/common/images/repo_private.png b/src/main/webapp/assets/common/images/repo_private.png new file mode 100644 index 0000000..5f5e8ad --- /dev/null +++ b/src/main/webapp/assets/common/images/repo_private.png Binary files differ diff --git a/src/main/webapp/assets/common/images/repo_private_lg.png b/src/main/webapp/assets/common/images/repo_private_lg.png new file mode 100644 index 0000000..cccc495 --- /dev/null +++ b/src/main/webapp/assets/common/images/repo_private_lg.png Binary files differ diff --git a/src/main/webapp/assets/common/images/repo_public.png b/src/main/webapp/assets/common/images/repo_public.png new file mode 100644 index 0000000..66f3f21 --- /dev/null +++ b/src/main/webapp/assets/common/images/repo_public.png Binary files differ diff --git a/src/main/webapp/assets/common/images/repo_public_lg.png b/src/main/webapp/assets/common/images/repo_public_lg.png new file mode 100644 index 0000000..4b5527e --- /dev/null +++ b/src/main/webapp/assets/common/images/repo_public_lg.png Binary files differ diff --git a/src/main/webapp/assets/common/js/gitbucket.js b/src/main/webapp/assets/common/js/gitbucket.js index 5c6233d..12e86fa 100644 --- a/src/main/webapp/assets/common/js/gitbucket.js +++ b/src/main/webapp/assets/common/js/gitbucket.js @@ -11,6 +11,26 @@ $('img[data-toggle=tooltip]').tooltip(); $('a[data-toggle=tooltip]').tooltip(); + // anchor icon for markdown + $('.markdown-head').mouseenter(function(e){ + $(e.target).children('a.markdown-anchor-link').show(); + }); + $('.markdown-head').mouseleave(function(e){ + var anchorLink = $(e.target).children('a.markdown-anchor-link'); + if(anchorLink.data('active') != true){ + anchorLink.hide(); + } + }); + + $('a.markdown-anchor-link').mouseenter(function(e){ + $(e.target).data('active', true); + }); + + $('a.markdown-anchor-link').mouseleave(function(e){ + $(e.target).data('active', false); + $(e.target).hide(); + }); + // syntax highlighting by google-code-prettify prettyPrint(); }); diff --git a/src/main/webapp/assets/jsdifflib/diffview.css b/src/main/webapp/assets/jsdifflib/diffview.css index 811a593..4c4a7c7 100644 --- a/src/main/webapp/assets/jsdifflib/diffview.css +++ b/src/main/webapp/assets/jsdifflib/diffview.css @@ -66,7 +66,7 @@ background-color:#FD8 } table.diff .delete { - background-color:#E99; + background-color:#FFDDDD; } table.diff .skip { background-color:#EFEFEF; @@ -74,10 +74,10 @@ border-right:1px solid #BBC; } table.diff .insert { - background-color:#9E9 + background-color:#DDFFDD } table.diff th.author { text-align:right; border-top:1px solid #BBC; background:#EFEFEF -} \ No newline at end of file +} diff --git a/src/test/scala/util/StringUtilSpec.scala b/src/test/scala/util/StringUtilSpec.scala index 26056a5..caaa2dc 100644 --- a/src/test/scala/util/StringUtilSpec.scala +++ b/src/test/scala/util/StringUtilSpec.scala @@ -35,4 +35,22 @@ 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 + } + } } diff --git a/src/test/scala/view/AvatarImageProviderSpec.scala b/src/test/scala/view/AvatarImageProviderSpec.scala index d5e5775..f337316 100644 --- a/src/test/scala/view/AvatarImageProviderSpec.scala +++ b/src/test/scala/view/AvatarImageProviderSpec.scala @@ -10,7 +10,7 @@ class AvatarImageProviderSpec extends Specification { - implicit val context = app.Context("", None, "", null) + implicit val context = app.Context("", None, null) "getAvatarImageHtml" should { "show Gravatar image for no image account if gravatar integration is enabled" in { @@ -80,6 +80,7 @@ private def createSystemSettings(useGravatar: Boolean) = SystemSettings( + baseUrl = None, allowAccountRegistration = false, gravatar = useGravatar, notification = false, diff --git a/src/test/scala/view/GitBucketHtmlSerializerSpec.scala b/src/test/scala/view/GitBucketHtmlSerializerSpec.scala index d82630d..946b626 100644 --- a/src/test/scala/view/GitBucketHtmlSerializerSpec.scala +++ b/src/test/scala/view/GitBucketHtmlSerializerSpec.scala @@ -16,13 +16,13 @@ "normalize characters with diacritics" in { val before = "Dónde estará mi vida" val after = generateAnchorName(before) - after mustEqual "donde-estara-mi-vida" + after mustEqual "do%cc%81nde-estara%cc%81-mi-vida" } "omit special characters" in { val before = "foo!bar@baz>9000" val after = generateAnchorName(before) - after mustEqual "foobarbaz9000" + after mustEqual "foo%21bar%40baz%3e9000" } } }