Sync upstream/maste to master and Merge branch 'master' into add-features-to-ldapauth

Conflicts:
	src/main/scala/app/IndexController.scala
	src/main/scala/service/SystemSettingsService.scala
	src/main/twirl/admin/system.scala.html
This commit is contained in:
yjkony
2014-03-03 15:46:38 +09:00
47 changed files with 977 additions and 593 deletions

View File

@@ -23,7 +23,7 @@ Following features are not implemented, but we will make them in the future rele
- 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 @@ or you can start GitBucket by `java -jar gitbucket.war` without servlet containe
- --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 @@ Run the following commands in `Terminal` to
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

View File

@@ -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

View File

@@ -39,9 +39,6 @@ start() {
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 &

View File

@@ -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" />
<rect
style="color:#000000;fill:none;stroke:#b3b3b3;stroke-width:9.34194565;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="rect3083"
width="170.93134"
height="207.72536"
x="38.526306"
y="1299.8645" />
<rect
style="color:#000000;fill:none;stroke:#b3b3b3;stroke-width:8.41239071;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="rect3083-7"
width="171.86089"
height="167.53221"
x="38.061527"
y="1300.4821" />
<rect
id="rect2995-0-4"
y="1301.3412"
x="42.553577"
height="163.64935"
width="29.769083"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-0"
y="1321.9025"
x="85.732407"
height="17.555511"
width="16.782965"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-0-9"
y="1356.7848"
x="85.732407"
height="17.555511"
width="16.782965"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-0-9-4"
y="1391.6671"
x="85.732407"
height="17.555511"
width="16.782965"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-0-9-4-8"
y="1426.5494"
x="85.732407"
height="17.555511"
width="16.782965"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-8"
y="1482.7141"
x="70.149086"
height="30.541632"
width="42.755199"
style="fill:#b3b3b3;stroke:none" />
<path
sodipodi:type="star"
style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:7.55999994;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="path4002"
sodipodi:sides="3"
sodipodi:cx="235.71429"
sodipodi:cy="1000.2193"
sodipodi:r1="15.016997"
sodipodi:r2="7.5084987"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 250.73129,1000.2193 -11.26275,6.5026 -11.26274,6.5025 0,-13.0051 0,-13.0051 11.26274,6.50255 z"
transform="matrix(1.0346242,0,0,1.5150471,-165.95814,-2.7851671)"
inkscape:transform-center-x="-2.5637799" />
<path
sodipodi:type="star"
style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:7.55999994;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="path4002-2"
sodipodi:sides="3"
sodipodi:cx="235.71429"
sodipodi:cy="1000.2193"
sodipodi:r1="15.016997"
sodipodi:r2="7.5084987"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 250.73129,1000.2193 -11.26275,6.5026 -11.26274,6.5025 0,-13.0051 0,-13.0051 11.26274,6.50255 z"
transform="matrix(-0.93510984,0,0,1.5150471,326.24502,-2.7851671)"
inkscape:transform-center-x="3.5106467" />
<rect
style="color:#000000;fill:none;stroke:#b3b3b3;stroke-width:9.34194565;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="rect3083-4"
width="170.93134"
height="207.72536"
x="280.50113"
y="1299.152" />
<rect
style="color:#000000;fill:none;stroke:#b3b3b3;stroke-width:8.41239071;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="rect3083-7-5"
width="171.86087"
height="167.53221"
x="280.03638"
y="1299.7695" />
<rect
id="rect2995-0-4-5"
y="1300.6287"
x="284.52841"
height="163.64934"
width="29.769083"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-8-5"
y="1482.0016"
x="312.12393"
height="30.541632"
width="42.755199"
style="fill:#b3b3b3;stroke:none" />
<path
sodipodi:type="star"
style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:7.55999994;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="path4002-27"
sodipodi:sides="3"
sodipodi:cx="235.71429"
sodipodi:cy="1000.2193"
sodipodi:r1="15.016997"
sodipodi:r2="7.5084987"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 250.73129,1000.2193 -11.26275,6.5026 -11.26274,6.5025 0,-13.0051 0,-13.0051 11.26274,6.50255 z"
transform="matrix(1.0346242,0,0,1.5150471,76.016678,-3.496726)"
inkscape:transform-center-x="-3.8842459"
inkscape:transform-center-y="-1.5464308e-005" />
<path
sodipodi:type="star"
style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:7.55999994;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="path4002-2-6"
sodipodi:sides="3"
sodipodi:cx="235.71429"
sodipodi:cy="1000.2193"
sodipodi:r1="15.016997"
sodipodi:r2="7.5084987"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 250.73129,1000.2193 -11.26275,6.5026 -11.26274,6.5025 0,-13.0051 0,-13.0051 11.26274,6.50255 z"
transform="matrix(-0.93510984,0,0,1.5150471,568.21986,-3.496726)"
inkscape:transform-center-x="5.318797"
inkscape:transform-center-y="-1.5464308e-005" />
<rect
id="rect2995-0-4-5-7"
y="1392.2405"
x="365.67133"
height="58.049755"
width="29.769083"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-5-7-6"
y="1319.5453"
x="326.67615"
height="49.632401"
width="29.769083"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-5-7-8"
y="1179.0293"
x="-767.54126"
height="58.049755"
width="29.769083"
style="fill:#b3b3b3;stroke:none"
transform="matrix(0.68860063,-0.7251408,0.7251408,0.68860063,0,0)" />
<rect
id="rect2995-0-4-5-7-6-9"
y="1319.5453"
x="403.28595"
height="49.632404"
width="29.769083"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-5-7-8-2"
y="623.14606"
x="-1287.8975"
height="55.681484"
width="28.564859"
style="fill:#b3b3b3;stroke:none"
transform="matrix(-0.68607628,-0.72752961,-0.72274236,0.69111755,0,0)" />
<rect
style="color:#000000;fill:#ffffff;stroke:#b3b3b3;stroke-width:7.29121827999999980;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;fill-opacity:1"
id="rect3083-7-5-7"
width="172.98204"
height="125.03616"
x="529.78156"
y="1383.6165" />
<rect
id="rect2995-0-4-5-9"
y="1385.3533"
x="663.37042"
height="123.85819"
width="38.18644"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-5-9-5"
y="1401.4539"
x="552.03174"
height="15.96297"
width="117.00352"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-5-9-5-4"
y="1437.4023"
x="551.16083"
height="15.96297"
width="117.00352"
style="fill:#b3b3b3;stroke:none" />
<rect
id="rect2995-0-4-5-9-5-4-3"
y="1473.7642"
x="551.16083"
height="15.96297"
width="117.00352"
style="fill:#b3b3b3;stroke:none" />
<path
style="fill:none;stroke:#b3b3b3;stroke-width:23.0681076;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 558.62308,1380.7989 0,-45.237 c 0,0 13.52904,-35.6384 56.38304,-36.1894 40.81922,-0.5248 55.47363,34.6931 55.47363,34.6931 l 0.17276,48.4719"
id="path4310"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccscc" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -25,8 +25,6 @@ public class JettyLauncher {
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 @@ public class JettyLauncher {
Server server = new Server();
HttpsSupportConnector connector = new HttpsSupportConnector(forceHttps);
SelectChannelConnector connector = new SelectChannelConnector();
if(host != null) {
connector.setHost(host);
}
@@ -62,19 +60,3 @@ public class JettyLauncher {
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,16 +5,13 @@ import util.{FileUtil, OneselfAuthenticator}
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])

View File

@@ -10,8 +10,7 @@ import org.json4s._
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 @@ import org.scalatra.i18n._
* 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 @@ abstract class ControllerBase extends ScalatraFilter
/**
* 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 @@ abstract class ControllerBase extends ScalatraFilter
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.

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.
* 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)))

View File

@@ -1,16 +1,14 @@
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
with RepositoryService with ActivityService with AccountService with UsersAuthenticator
trait IndexControllerBase extends ControllerBase {
self: RepositoryService with SystemSettingsService with ActivityService with AccountService with UsersAuthenticator =>
self: RepositoryService with ActivityService with AccountService with UsersAuthenticator =>
case class SignInForm(userName: String, password: String)
@@ -32,7 +30,7 @@ trait IndexControllerBase extends ControllerBase {
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())
}
@@ -57,11 +55,10 @@ trait IndexControllerBase extends ControllerBase {
updateLastLoginDate(account.userName)
if(AccountUtil.hasLdapDummyMailAddress(account)) {
session.remove(Keys.Session.Redirect)
redirect("/" + account.userName + "/_edit")
}
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 {

View File

@@ -4,10 +4,11 @@ import jp.sf.amateras.scalatra.forms._
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 @@ trait IssuesControllerBase extends ControllerBase {
// 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 @@ trait IssuesControllerBase extends ControllerBase {
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 @@ trait IssuesControllerBase extends ControllerBase {
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 @@ trait IssuesControllerBase extends ControllerBase {
}
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 =>

View File

@@ -79,7 +79,7 @@ trait PullRequestsControllerBase extends ControllerBase {
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 @@ 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
getWebHookURLs(owner, name) match {
case webHookURLs if(webHookURLs.nonEmpty) =>

View File

@@ -5,7 +5,6 @@ import util.Directory._
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 @@ class RepositorySettingsController extends RepositorySettingsControllerBase
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 =>

View File

@@ -276,7 +276,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
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,

View File

@@ -6,13 +6,10 @@ import service._
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)),

View File

@@ -4,15 +4,15 @@ import service.{AccountService, SystemSettingsService}
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())),

View File

@@ -6,18 +6,15 @@ import util.Directory._
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)

View File

@@ -65,7 +65,7 @@ trait AccountService {
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){

View File

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

View File

@@ -3,11 +3,19 @@ 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)
@@ -50,6 +58,7 @@ trait SystemSettingsService {
props.load(new java.io.FileInputStream(GitBucketConf))
}
SystemSettings(
getOptionValue(props, BaseURL, None),
getValue(props, AllowAccountRegistration, false),
getValue(props, Gravatar, true),
getValue(props, Notification, false),
@@ -93,6 +102,7 @@ object SystemSettingsService {
import scala.reflect.ClassTag
case class SystemSettings(
baseUrl: Option[String],
allowAccountRegistration: Boolean,
gravatar: Boolean,
notification: Boolean,
@@ -126,6 +136,7 @@ object SystemSettingsService {
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"

View File

@@ -50,6 +50,7 @@ object AutoUpdate {
* 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),

View File

@@ -9,7 +9,7 @@ import org.slf4j.LoggerFactory
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,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])
@@ -64,13 +64,11 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
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 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
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 @@ 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
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 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseURL:
}
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 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseURL:
*/
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)

View File

@@ -3,7 +3,7 @@ package util
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 @@ object ControlUtil {
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
}
}
}

View File

@@ -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 @@ object Implicits {
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)
}
}

View File

@@ -128,7 +128,7 @@ object JGitUtil {
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",

View File

@@ -13,12 +13,7 @@ object Keys {
/**
* 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 Keys {
}
object Flash {
/**
* Flash key for the redirect URL.
*/
val Redirect = "redirect"
/**
* Flash key for the information message.
*/
val Info = "info"
}
/**
* 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]].
* 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 @@ object StringUtil {
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)(?<!\\w)(?:fix(?:e[sd])?|resolve[sd]?|close[sd]?)\\s+#(\\d+)(?!\\w)".r.findAllIn(message).matchData.map(_.group(1))
}

View File

@@ -116,8 +116,9 @@ class GitBucketHtmlSerializer(
val tag = s"h${node.getLevel}"
val headerTextString = printChildrenToString(node)
val anchorName = GitBucketHtmlSerializer.generateAnchorName(headerTextString)
printer.print(s"<$tag>")
printer.print(s"""<a class="anchor" name="$anchorName" href="#$anchorName"></a>""")
printer.print(s"""<$tag class="markdown-head">""")
printer.print(s"""<a class="markdown-anchor-link" href="#$anchorName"></a>""")
printer.print(s"""<a class="markdown-anchor" name="$anchorName"></a>""")
visitChildren(node)
printer.print(s"</$tag>")
}
@@ -142,12 +143,10 @@ object GitBucketHtmlSerializer {
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)
}
}

View File

@@ -15,6 +15,21 @@
<label class="strong">GITBUCKET_HOME</label>
@GitBucketHome
<!--====================================================================-->
<!-- Base URL -->
<!--====================================================================-->
<hr>
<label><span class="strong">Base URL</span> (e.g. <code>http://example.com/gitbucket</code>)</label>
<fieldset>
<div class="controls">
<input type="text" name="baseUrl" id="baseUrl" style="width: 400px" value="@settings.baseUrl"/>
</div>
</fieldset>
<p>
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.
</p>
<!--====================================================================-->
<!-- Account registration -->
<!--====================================================================-->
<hr>
@@ -95,7 +110,7 @@
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapAdditionalFilterCondition">Additional filter condition</label>
<label class="control-label" for="ldapAdditionalFilterCondition">Full name attribute</label>
<div class="controls">
<input type="text" id="ldapAdditionalFilterCondition" name="ldap.additionalFilterCondition" value="@settings.ldap.map(_.additionalFilterCondition)"/>
<span id="error-ldap_additionalFilterCondition" class="error"></span>
@@ -118,7 +133,7 @@
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" name="ldap.disableMailResolve"@if(settings.ldap.flatMap(_.disableMailResolve).getOrElse(false)){ checked}/> Disable Mail Resolve
<input type="checkbox" name="ldap.disableMailResolve"@if(settings.ldap.flatMap(_.disableMailResolve).getOrElse(false)){ checked}/> Disable mail resolve
</label>
</div>
</div>

View File

@@ -12,10 +12,13 @@
}
<div class="head">
@if(repository.repository.isPrivate){
<i class="icon-lock"></i>
<img src="@assets/common/images/repo_private_lg.png"/>
} else {
@if(repository.repository.originUserName.isDefined){
<img src="@assets/common/images/repo_fork_lg.png"/>
} else {
<img src="@assets/common/images/repo_public_lg.png"/>
}
@if(!repository.repository.isPrivate){
<i class="icon-eye-open"></i>
}
<a href="@url(repository.owner)">@repository.owner</a> / <a href="@url(repository)" class="strong">@repository.name</a>
@@ -27,6 +30,9 @@
}
}
</div>
@repository.repository.description.map { description =>
<p>@description</p>
}
<table class="global-nav box-header">
<tr>
<th class="box-header@if(active=="code"){ active}">

View File

@@ -32,10 +32,13 @@
<tr>
<td>
@if(repository.repository.isPrivate){
<i class="icon-lock"></i>
<img src="@assets/common/images/repo_private.png"/>
} else {
@if(repository.repository.originUserName.isDefined){
<img src="@assets/common/images/repo_fork.png"/>
} else {
<img src="@assets/common/images/repo_public.png"/>
}
@if(!repository.repository.isPrivate){
<i class="icon-eye-open"></i>
}
@if(repository.owner == loginAccount.get.userName){
<a href="@url(repository)"><span class="strong">@repository.name</span></a>
@@ -64,10 +67,13 @@
<tr>
<td>
@if(repository.repository.isPrivate){
<i class="icon-lock"></i>
<img src="@assets/common/images/repo_private.png"/>
} else {
@if(repository.repository.originUserName.isDefined){
<img src="@assets/common/images/repo_fork.png"/>
} else {
<img src="@assets/common/images/repo_public.png"/>
}
@if(!repository.repository.isPrivate){
<i class="icon-eye-open"></i>
}
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
</td>

View File

@@ -11,10 +11,16 @@
<div class="box issue-comment-box" id="comment-@comment.commentId">
<div class="box-header-small">
<i class="icon-comment"></i>
@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 }
}
<span class="pull-right">
@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))){
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>&nbsp;
<a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle"></i></a>
}
@@ -26,9 +32,15 @@
<div class="pull-right"><a href="@path/@repository.owner/@repository.name/commit/@id" class="monospace">@id.substring(0, 7)</a></div>
@markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true)
}
} else {
@if(comment.action == "refer"){
@defining(comment.content.split(":")){ case Array(issueId, rest @ _*) =>
<strong><a href="@path/@repository.owner/@repository.name/issues/@issueId">Issue #@issueId</a>: @rest.mkString(":")</strong>
}
} else {
@markdown(comment.content, repository, false, true)
}
}
</div>
</div>
}

View File

@@ -61,7 +61,7 @@
}
<a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a>
} else {
<a href="@path/signin?redirect=@redirectUrl" class="btn btn-last">Sign in</a>
<a href="@path/signin" class="btn btn-last" id="signin">Sign in</a>
}
</div><!--/.nav-collapse -->
</div>
@@ -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));
});
</script>
</body>

View File

@@ -30,7 +30,7 @@
<fieldset class="margin">
<label class="radio">
<input type="radio" name="isPrivate" value="false" checked>
<span class="strong"><i class="icon-eye-open">&nbsp;</i>&nbsp;Public</span><br>
<span class="strong"><img src="@assets/common/images/repo_public.png"/>&nbsp;</i>&nbsp;Public</span><br>
<div>
<span>All users and guests can read this repository.</span>
</div>
@@ -39,7 +39,7 @@
<fieldset>
<label class="radio">
<input type="radio" name="isPrivate" value="true">
<span class="strong"><i class="icon-lock">&nbsp;</i>&nbsp;Private</span><br>
<span class="strong"><img src="@assets/common/images/repo_private.png"/>&nbsp;</i>&nbsp;Private</span><br>
<div>
<span>Only collaborators can read this repository.</span>
</div>

View File

@@ -3,7 +3,7 @@
pathList: List[String],
latestCommit: util.JGitUtil.CommitInfo,
files: List[util.JGitUtil.FileInfo],
readme: Option[String])(implicit context: app.Context)
readme: Option[(util.JGitUtil.FileInfo, String)])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@@ -12,7 +12,7 @@
<div class="head">
<div class="pull-right">
@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>
<a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> /
@@ -77,9 +77,9 @@
</table>
</div>
@readme.map { content =>
@readme.map { case(file, content) =>
<div id="readme" class="box">
<div class="box-header">README.md</div>
<div class="box-header">@file.name</div>
<div class="box-content markdown-body">@markdown(content, repository, false, false)</div>
</div>
}

View File

@@ -850,3 +850,21 @@ div.markdown-body table colgroup + tbody tr:first-child td:last-child {
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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 B

View File

@@ -11,6 +11,26 @@ $(function(){
$('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();
});

View File

@@ -66,7 +66,7 @@ table.diff .replace {
background-color:#FD8
}
table.diff .delete {
background-color:#E99;
background-color:#FFDDDD;
}
table.diff .skip {
background-color:#EFEFEF;
@@ -74,7 +74,7 @@ table.diff .skip {
border-right:1px solid #BBC;
}
table.diff .insert {
background-color:#9E9
background-color:#DDFFDD
}
table.diff th.author {
text-align:right;

View File

@@ -35,4 +35,22 @@ class StringUtilSpec extends Specification {
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
}
}
}

View File

@@ -10,7 +10,7 @@ import twirl.api.Html
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 @@ class AvatarImageProviderSpec extends Specification {
private def createSystemSettings(useGravatar: Boolean) =
SystemSettings(
baseUrl = None,
allowAccountRegistration = false,
gravatar = useGravatar,
notification = false,

View File

@@ -16,13 +16,13 @@ class GitBucketHtmlSerializerSpec extends Specification {
"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"
}
}
}