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 - File editing in repository viewer
- Comment for the changeset - Comment for the changeset
- Network graph - Network graph
- Statics - Statistics
- Watch / Star - 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). 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] - --port=[NUMBER]
- --prefix=[CONTEXTPATH] - --prefix=[CONTEXTPATH]
- --host=[HOSTNAME] - --host=[HOSTNAME]
- --https=true
- --gitbucket.home=[DATA_DIR] - --gitbucket.home=[DATA_DIR]
To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/.gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk. To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/.gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk.
@@ -59,12 +58,24 @@ Run the following commands in `Terminal` to
Release Notes 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 ### 1.10 - 01 Feb 2014
- Rename repository - Rename repository
- Transfer repository owner - 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. - 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 - Add LDAP display name attribute
- Response improvement - Response performance improvement
- Fix some bugs - Fix some bugs
### 1.9 - 28 Dec 2013 ### 1.9 - 28 Dec 2013

View File

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

View File

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

View File

@@ -25,17 +25,17 @@
inkscape:pageopacity="0.0" inkscape:pageopacity="0.0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:zoom="1.4" inkscape:zoom="1.4"
inkscape:cx="629.30023" inkscape:cx="450.21999"
inkscape:cy="281.44758" inkscape:cy="97.51519"
inkscape:document-units="px" inkscape:document-units="px"
inkscape:current-layer="layer1-9" inkscape:current-layer="layer1-9"
showgrid="false" showgrid="false"
inkscape:window-width="1366" inkscape:window-width="1366"
inkscape:window-height="705" inkscape:window-height="706"
inkscape:window-x="-8" inkscape:window-x="1912"
inkscape:window-y="-8" inkscape:window-y="-8"
inkscape:window-maximized="1" inkscape:window-maximized="1"
inkscape:snap-global="true" inkscape:snap-global="false"
inkscape:snap-grids="false" inkscape:snap-grids="false"
inkscape:snap-page="false" inkscape:snap-page="false"
inkscape:snap-bbox="true" 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" 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" id="rect2995-0-2-7-7"
inkscape:connector-curvature="0" /> 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>
</g> </g>
</svg> </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]); port = Integer.parseInt(dim[1]);
} else if(dim[0].equals("--prefix")) { } else if(dim[0].equals("--prefix")) {
contextPath = dim[1]; contextPath = dim[1];
} else if(dim[0].equals("--https") && (dim[1].equals("1") || dim[1].equals("true"))) {
forceHttps = true;
} else if(dim[0].equals("--gitbucket.home")){ } else if(dim[0].equals("--gitbucket.home")){
System.setProperty("gitbucket.home", dim[1]); System.setProperty("gitbucket.home", dim[1]);
} }
@@ -36,7 +34,7 @@ public class JettyLauncher {
Server server = new Server(); Server server = new Server();
HttpsSupportConnector connector = new HttpsSupportConnector(forceHttps); SelectChannelConnector connector = new SelectChannelConnector();
if(host != null) { if(host != null) {
connector.setHost(host); connector.setHost(host);
} }
@@ -62,19 +60,3 @@ public class JettyLauncher {
server.join(); server.join();
} }
} }
class HttpsSupportConnector extends SelectChannelConnector {
private boolean forceHttps;
public HttpsSupportConnector(boolean forceHttps) {
this.forceHttps = forceHttps;
}
@Override
public void customize(final EndPoint endpoint, final Request request) throws IOException {
if (this.forceHttps) {
request.setScheme("https");
super.customize(endpoint, request);
}
}
}

View File

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

View File

@@ -10,8 +10,7 @@ import org.json4s._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import model.Account import model.Account
import scala.Some import service.{SystemSettingsService, AccountService}
import service.AccountService
import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest} import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest}
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import javax.servlet.{FilterChain, ServletResponse, ServletRequest} import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
@@ -21,7 +20,8 @@ import org.scalatra.i18n._
* Provides generic features for controller implementations. * Provides generic features for controller implementations.
*/ */
abstract class ControllerBase extends ScalatraFilter abstract class ControllerBase extends ScalatraFilter
with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with Validations { with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations
with SystemSettingsService {
implicit val jsonFormats = DefaultFormats implicit val jsonFormats = DefaultFormats
@@ -58,11 +58,7 @@ abstract class ControllerBase extends ScalatraFilter
/** /**
* Returns the context object for the request. * Returns the context object for the request.
*/ */
implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL, request) implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, request)
private def currentURL: String = defining(request.getQueryString){ queryString =>
request.getRequestURI + (if(queryString != null) "?" + queryString else "")
}
private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount) 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"){ if(request.getMethod.toUpperCase == "POST"){
org.scalatra.Unauthorized(redirect("/signin")) org.scalatra.Unauthorized(redirect("/signin"))
} else { } 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 => override def fullUrl(path: String, params: Iterable[(String, Any)] = Iterable.empty,
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length)) 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. * Context object for the current request.
*/ */
case class Context(path: String, loginAccount: Option[Account], currentUrl: String, request: HttpServletRequest){ case class Context(path: String, loginAccount: Option[Account], request: HttpServletRequest){
def redirectUrl = if(request.getParameter("redirect") != null){
request.getParameter("redirect")
} else {
currentUrl
}
/** /**
* Get object from cache. * 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. * This servlet saves uploaded file as temporary file and returns the unique id.
* You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id. * You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id.
*/ */
class FileUploadController extends ScalatraServlet class FileUploadController extends ScalatraServlet with FileUploadSupport with FileUploadControllerBase {
with FileUploadSupport with FlashMapSupport with FileUploadControllerBase {
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024))) configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))

View File

@@ -1,88 +1,85 @@
package app package app
import util._ import util._
import util.Implicits._ import service._
import service._ import jp.sf.amateras.scalatra.forms._
import jp.sf.amateras.scalatra.forms._
class IndexController extends IndexControllerBase
class IndexController extends IndexControllerBase with RepositoryService with ActivityService with AccountService with UsersAuthenticator
with RepositoryService with SystemSettingsService with ActivityService with AccountService
with UsersAuthenticator trait IndexControllerBase extends ControllerBase {
self: RepositoryService 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)
case class SignInForm(userName: String, password: String) val form = mapping(
"userName" -> trim(label("Username", text(required))),
val form = mapping( "password" -> trim(label("Password", text(required)))
"userName" -> trim(label("Username", text(required))), )(SignInForm.apply)
"password" -> trim(label("Password", text(required)))
)(SignInForm.apply) get("/"){
val loginAccount = context.loginAccount
get("/"){
val loginAccount = context.loginAccount html.index(getRecentActivities(),
getVisibleRepositories(loginAccount, baseUrl),
html.index(getRecentActivities(), loadSystemSettings(),
getVisibleRepositories(loginAccount, baseUrl), loginAccount.map{ account => getUserRepositories(account.userName, baseUrl) }.getOrElse(Nil)
loadSystemSettings(), )
loginAccount.map{ account => getUserRepositories(account.userName, baseUrl) }.getOrElse(Nil) }
)
} get("/signin"){
val redirect = params.get("redirect")
get("/signin"){ if(redirect.isDefined && redirect.get.startsWith("/")){
val redirect = params.get("redirect") flash += Keys.Flash.Redirect -> redirect.get
if(redirect.isDefined && redirect.get.startsWith("/")){ }
session.setAttribute(Keys.Session.Redirect, redirect.get) html.signin(loadSystemSettings())
} }
html.signin(loadSystemSettings())
} post("/signin", form){ form =>
authenticate(loadSystemSettings(), form.userName, form.password) match {
post("/signin", form){ form => case Some(account) => signin(account)
authenticate(loadSystemSettings(), form.userName, form.password) match { case None => redirect("/signin")
case Some(account) => signin(account) }
case None => redirect("/signin") }
}
} get("/signout"){
session.invalidate
get("/signout"){ redirect("/")
session.invalidate }
redirect("/")
} /**
* Set account information into HttpSession and redirect.
/** */
* Set account information into HttpSession and redirect. private def signin(account: model.Account) = {
*/ session.setAttribute(Keys.Session.LoginAccount, account)
private def signin(account: model.Account) = { updateLastLoginDate(account.userName)
session.setAttribute(Keys.Session.LoginAccount, account)
updateLastLoginDate(account.userName) if(AccountUtil.hasLdapDummyMailAddress(account)) {
redirect("/" + account.userName + "/_edit")
if(AccountUtil.hasLdapDummyMailAddress(account)) { }
session.remove(Keys.Session.Redirect)
redirect("/" + account.userName + "/_edit") flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl =>
} if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){
redirect("/")
session.getAndRemove[String](Keys.Session.Redirect).map { redirectUrl => } else {
if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){ redirect(redirectUrl)
redirect("/") }
} else { }.getOrElse {
redirect(redirectUrl) redirect("/")
} }
}.getOrElse { }
redirect("/")
} /**
} * JSON API for collaborator completion.
*
/** * TODO Move to other controller?
* JSON API for collaborator completion. */
* get("/_user/proposals")(usersOnly {
* TODO Move to other controller? contentType = formats("json")
*/ org.json4s.jackson.Serialization.write(
get("/_user/proposals")(usersOnly { Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray)
contentType = formats("json") )
org.json4s.jackson.Serialization.write( })
Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray)
)
}) }
}

View File

@@ -4,10 +4,11 @@ import jp.sf.amateras.scalatra.forms._
import service._ import service._
import IssuesService._ import IssuesService._
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator, Notifier, Keys} import util._
import util.Implicits._ import util.Implicits._
import util.ControlUtil._ import util.ControlUtil._
import org.scalatra.Ok import org.scalatra.Ok
import model.Issue
class IssuesController extends IssuesControllerBase class IssuesController extends IssuesControllerBase
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
@@ -110,6 +111,11 @@ trait IssuesControllerBase extends ControllerBase {
// record activity // record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title) 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 // notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){ Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgIssue(s"${baseUrl}/${owner}/${name}/issues/${issueId}") Notifier.msgIssue(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
@@ -123,7 +129,11 @@ trait IssuesControllerBase extends ControllerBase {
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
getIssue(owner, name, params("id")).map { issue => getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){ if(isEditable(owner, name, issue.openedUserName)){
// update issue
updateIssue(owner, name, issue.issueId, form.title, form.content) 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}") redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized } else Unauthorized
} getOrElse NotFound } getOrElse NotFound
@@ -274,6 +284,15 @@ trait IssuesControllerBase extends ControllerBase {
redirect(s"/${repository.owner}/${repository.name}/issues") 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]] * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
*/ */
@@ -313,6 +332,11 @@ trait IssuesControllerBase extends ControllerBase {
} }
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) ) recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
// extract references and create refer comment
content.map { content =>
createReferComment(owner, name, issue, content)
}
// notifications // notifications
Notifier() match { Notifier() match {
case f => case f =>

View File

@@ -79,7 +79,7 @@ trait PullRequestsControllerBase extends ControllerBase {
pulls.html.pullreq( pulls.html.pullreq(
issue, pullreq, issue, pullreq,
getComments(owner, name, issueId), getComments(owner, name, issueId),
getIssueLabels(owner, name, issueId.toInt), getIssueLabels(owner, name, issueId),
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestonesWithIssueCount(owner, name), getMilestonesWithIssueCount(owner, name),
getLabels(owner, name), getLabels(owner, name),
@@ -183,6 +183,18 @@ trait PullRequestsControllerBase extends ControllerBase {
} }
} }
// close issue by content of pull request
val defaultBranch = getRepository(owner, name, baseUrl).get.repository.defaultBranch
if(pullreq.branch == defaultBranch){
commits.flatten.foreach { commit =>
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
}
issue.content match {
case Some(content) => closeIssuesFromMessage(content, loginAccount.userName, owner, name)
case _ =>
}
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
}
// call web hook // call web hook
getWebHookURLs(owner, name) match { getWebHookURLs(owner, name) match {
case webHookURLs if(webHookURLs.nonEmpty) => case webHookURLs if(webHookURLs.nonEmpty) =>

View File

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

View File

@@ -276,7 +276,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val readme = files.find { file => val readme = files.find { file =>
readmeFiles.contains(file.name.toLowerCase) readmeFiles.contains(file.name.toLowerCase)
}.map { file => }.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, repo.html.files(revision, repository,

View File

@@ -6,13 +6,10 @@ import service._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
class SearchController extends SearchControllerBase class SearchController extends SearchControllerBase
with RepositoryService with AccountService with SystemSettingsService with ActivityService with RepositoryService with AccountService with ActivityService with RepositorySearchService with IssuesService with ReferrerAuthenticator
with RepositorySearchService with IssuesService
with ReferrerAuthenticator
trait SearchControllerBase extends ControllerBase { self: RepositoryService trait SearchControllerBase extends ControllerBase { self: RepositoryService
with SystemSettingsService with ActivityService with RepositorySearchService with ActivityService with RepositorySearchService with ReferrerAuthenticator =>
with ReferrerAuthenticator =>
val searchForm = mapping( val searchForm = mapping(
"query" -> trim(text(required)), "query" -> trim(text(required)),

View File

@@ -4,15 +4,15 @@ import service.{AccountService, SystemSettingsService}
import SystemSettingsService._ import SystemSettingsService._
import util.AdminAuthenticator import util.AdminAuthenticator
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.scalatra.FlashMapSupport
class SystemSettingsController extends SystemSettingsControllerBase class SystemSettingsController extends SystemSettingsControllerBase
with SystemSettingsService with AccountService with AdminAuthenticator with SystemSettingsService with AccountService with AdminAuthenticator
trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport { trait SystemSettingsControllerBase extends ControllerBase {
self: SystemSettingsService with AccountService with AdminAuthenticator => self: SystemSettingsService with AccountService with AdminAuthenticator =>
private val form = mapping( private val form = mapping(
"baseUrl" -> trim(label("Base URL", optional(text()))),
"allowAccountRegistration" -> trim(label("Account registration", boolean())), "allowAccountRegistration" -> trim(label("Account registration", boolean())),
"gravatar" -> trim(label("Gravatar", boolean())), "gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())), "notification" -> trim(label("Notification", boolean())),

View File

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

View File

@@ -65,7 +65,7 @@ trait AccountService {
Query(Accounts) filter(t => (t.userName is userName.bind) && (t.removed is false.bind, !includeRemoved)) firstOption Query(Accounts) filter(t => (t.userName is userName.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false): Option[Account] = def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false): Option[Account] =
Query(Accounts) filter(t => (t.mailAddress is mailAddress.bind) && (t.removed is false.bind, !includeRemoved)) firstOption Query(Accounts) filter(t => (t.mailAddress.toLowerCase is mailAddress.toLowerCase.bind) && (t.removed is false.bind, !includeRemoved)) firstOption
def getAllUsers(includeRemoved: Boolean = true): List[Account] = def getAllUsers(includeRemoved: Boolean = true): List[Account] =
if(includeRemoved){ if(includeRemoved){

View File

@@ -8,6 +8,7 @@ import Q.interpolation
import model._ import model._
import util.Implicits._ import util.Implicits._
import util.StringUtil._ import util.StringUtil._
import util.StringUtil
trait IssuesService { trait IssuesService {
import IssuesService._ import IssuesService._
@@ -314,6 +315,14 @@ trait IssuesService {
}.toList }.toList
} }
def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String) = {
StringUtil.extractCloseId(message).foreach { issueId =>
for(issue <- getIssue(owner, repository, issueId) if !issue.closed){
createComment(owner, repository, userName, issue.issueId, "Close", "close")
updateClosed(owner, repository, issue.issueId, true)
}
}
}
} }
object IssuesService { object IssuesService {

View File

@@ -1,172 +1,183 @@
package service package service
import util.Directory._ import util.Directory._
import util.ControlUtil._ import util.ControlUtil._
import SystemSettingsService._ import SystemSettingsService._
import javax.servlet.http.HttpServletRequest
trait SystemSettingsService {
trait SystemSettingsService {
def saveSystemSettings(settings: SystemSettings): Unit = {
defining(new java.util.Properties()){ props => def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings().baseUrl.getOrElse {
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString) defining(request.getRequestURL.toString){ url =>
props.setProperty(Gravatar, settings.gravatar.toString) url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
props.setProperty(Notification, settings.notification.toString) }
if(settings.notification) { }.replaceFirst("/$", "")
settings.smtp.foreach { smtp =>
props.setProperty(SmtpHost, smtp.host) def saveSystemSettings(settings: SystemSettings): Unit = {
smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString)) defining(new java.util.Properties()){ props =>
smtp.user.foreach(props.setProperty(SmtpUser, _)) settings.baseUrl.foreach(props.setProperty(BaseURL, _))
smtp.password.foreach(props.setProperty(SmtpPassword, _)) props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString)) props.setProperty(Gravatar, settings.gravatar.toString)
smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _)) props.setProperty(Notification, settings.notification.toString)
smtp.fromName.foreach(props.setProperty(SmtpFromName, _)) if(settings.notification) {
} settings.smtp.foreach { smtp =>
} props.setProperty(SmtpHost, smtp.host)
props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString) smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString))
if(settings.ldapAuthentication){ smtp.user.foreach(props.setProperty(SmtpUser, _))
settings.ldap.map { ldap => smtp.password.foreach(props.setProperty(SmtpPassword, _))
props.setProperty(LdapHost, ldap.host) smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString))
ldap.port.foreach(x => props.setProperty(LdapPort, x.toString)) smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _))
ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x)) smtp.fromName.foreach(props.setProperty(SmtpFromName, _))
ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x)) }
props.setProperty(LdapBaseDN, ldap.baseDN) }
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute) props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString)
ldap.additionalFilterCondition.foreach(x => props.setProperty(LdapAdditionalFilterCondition, x)) if(settings.ldapAuthentication){
ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x)) settings.ldap.map { ldap =>
props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute) props.setProperty(LdapHost, ldap.host)
ldap.disableMailResolve.foreach(x => props.setProperty(LdapDisableMailResolve, x.toString)) ldap.port.foreach(x => props.setProperty(LdapPort, x.toString))
ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString)) ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x))
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x)) ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
} props.setProperty(LdapBaseDN, ldap.baseDN)
} props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
props.store(new java.io.FileOutputStream(GitBucketConf), null) 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))
def loadSystemSettings(): SystemSettings = { ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
defining(new java.util.Properties()){ props => }
if(GitBucketConf.exists){ }
props.load(new java.io.FileInputStream(GitBucketConf)) props.store(new java.io.FileOutputStream(GitBucketConf), null)
} }
SystemSettings( }
getValue(props, AllowAccountRegistration, false),
getValue(props, Gravatar, true),
getValue(props, Notification, false), def loadSystemSettings(): SystemSettings = {
if(getValue(props, Notification, false)){ defining(new java.util.Properties()){ props =>
Some(Smtp( if(GitBucketConf.exists){
getValue(props, SmtpHost, ""), props.load(new java.io.FileInputStream(GitBucketConf))
getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)), }
getOptionValue(props, SmtpUser, None), SystemSettings(
getOptionValue(props, SmtpPassword, None), getOptionValue(props, BaseURL, None),
getOptionValue[Boolean](props, SmtpSsl, None), getValue(props, AllowAccountRegistration, false),
getOptionValue(props, SmtpFromAddress, None), getValue(props, Gravatar, true),
getOptionValue(props, SmtpFromName, None))) getValue(props, Notification, false),
} else { if(getValue(props, Notification, false)){
None Some(Smtp(
}, getValue(props, SmtpHost, ""),
getValue(props, LdapAuthentication, false), getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
if(getValue(props, LdapAuthentication, false)){ getOptionValue(props, SmtpUser, None),
Some(Ldap( getOptionValue(props, SmtpPassword, None),
getValue(props, LdapHost, ""), getOptionValue[Boolean](props, SmtpSsl, None),
getOptionValue(props, LdapPort, Some(DefaultLdapPort)), getOptionValue(props, SmtpFromAddress, None),
getOptionValue(props, LdapBindDN, None), getOptionValue(props, SmtpFromName, None)))
getOptionValue(props, LdapBindPassword, None), } else {
getValue(props, LdapBaseDN, ""), None
getValue(props, LdapUserNameAttribute, ""), },
getOptionValue(props, LdapAdditionalFilterCondition, None), getValue(props, LdapAuthentication, false),
getOptionValue(props, LdapFullNameAttribute, None), if(getValue(props, LdapAuthentication, false)){
getValue(props, LdapMailAddressAttribute, ""), Some(Ldap(
getOptionValue[Boolean](props, LdapDisableMailResolve, None), getValue(props, LdapHost, ""),
getOptionValue[Boolean](props, LdapTls, None), getOptionValue(props, LdapPort, Some(DefaultLdapPort)),
getOptionValue(props, LdapKeystore, None))) getOptionValue(props, LdapBindDN, None),
} else { getOptionValue(props, LdapBindPassword, None),
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)))
object SystemSettingsService { } else {
import scala.reflect.ClassTag None
}
case class SystemSettings( )
allowAccountRegistration: Boolean, }
gravatar: Boolean, }
notification: Boolean,
smtp: Option[Smtp], }
ldapAuthentication: Boolean,
ldap: Option[Ldap]) object SystemSettingsService {
import scala.reflect.ClassTag
case class Ldap(
host: String, case class SystemSettings(
port: Option[Int], baseUrl: Option[String],
bindDN: Option[String], allowAccountRegistration: Boolean,
bindPassword: Option[String], gravatar: Boolean,
baseDN: String, notification: Boolean,
userNameAttribute: String, smtp: Option[Smtp],
additionalFilterCondition: Option[String], ldapAuthentication: Boolean,
fullNameAttribute: Option[String], ldap: Option[Ldap])
mailAttribute: String,
disableMailResolve: Option[Boolean], case class Ldap(
tls: Option[Boolean], host: String,
keystore: Option[String]) port: Option[Int],
bindDN: Option[String],
case class Smtp( bindPassword: Option[String],
host: String, baseDN: String,
port: Option[Int], userNameAttribute: String,
user: Option[String], additionalFilterCondition: Option[String],
password: Option[String], fullNameAttribute: Option[String],
ssl: Option[Boolean], mailAttribute: String,
fromAddress: Option[String], disableMailResolve: Option[Boolean],
fromName: Option[String]) tls: Option[Boolean],
keystore: Option[String])
val DefaultSmtpPort = 25
val DefaultLdapPort = 389 case class Smtp(
host: String,
private val AllowAccountRegistration = "allow_account_registration" port: Option[Int],
private val Gravatar = "gravatar" user: Option[String],
private val Notification = "notification" password: Option[String],
private val SmtpHost = "smtp.host" ssl: Option[Boolean],
private val SmtpPort = "smtp.port" fromAddress: Option[String],
private val SmtpUser = "smtp.user" fromName: Option[String])
private val SmtpPassword = "smtp.password"
private val SmtpSsl = "smtp.ssl" val DefaultSmtpPort = 25
private val SmtpFromAddress = "smtp.from_address" val DefaultLdapPort = 389
private val SmtpFromName = "smtp.from_name"
private val LdapAuthentication = "ldap_authentication" private val BaseURL = "base_url"
private val LdapHost = "ldap.host" private val AllowAccountRegistration = "allow_account_registration"
private val LdapPort = "ldap.port" private val Gravatar = "gravatar"
private val LdapBindDN = "ldap.bindDN" private val Notification = "notification"
private val LdapBindPassword = "ldap.bind_password" private val SmtpHost = "smtp.host"
private val LdapBaseDN = "ldap.baseDN" private val SmtpPort = "smtp.port"
private val LdapUserNameAttribute = "ldap.username_attribute" private val SmtpUser = "smtp.user"
private val LdapAdditionalFilterCondition = "ldap.additional_filter_condition" private val SmtpPassword = "smtp.password"
private val LdapFullNameAttribute = "ldap.fullname_attribute" private val SmtpSsl = "smtp.ssl"
private val LdapMailAddressAttribute = "ldap.mail_attribute" private val SmtpFromAddress = "smtp.from_address"
private val LdapDisableMailResolve = "ldap.disable_mail_resolve" private val SmtpFromName = "smtp.from_name"
private val LdapTls = "ldap.tls" private val LdapAuthentication = "ldap_authentication"
private val LdapKeystore = "ldap.keystore" private val LdapHost = "ldap.host"
private val LdapPort = "ldap.port"
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = private val LdapBindDN = "ldap.bindDN"
defining(props.getProperty(key)){ value => private val LdapBindPassword = "ldap.bind_password"
if(value == null || value.isEmpty) default private val LdapBaseDN = "ldap.baseDN"
else convertType(value).asInstanceOf[A] private val LdapUserNameAttribute = "ldap.username_attribute"
} private val LdapAdditionalFilterCondition = "ldap.additional_filter_condition"
private val LdapFullNameAttribute = "ldap.fullname_attribute"
private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] = private val LdapMailAddressAttribute = "ldap.mail_attribute"
defining(props.getProperty(key)){ value => private val LdapDisableMailResolve = "ldap.disable_mail_resolve"
if(value == null || value.isEmpty) default private val LdapTls = "ldap.tls"
else Some(convertType(value)).asInstanceOf[Option[A]] private val LdapKeystore = "ldap.keystore"
}
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
private def convertType[A: ClassTag](value: String) = defining(props.getProperty(key)){ value =>
defining(implicitly[ClassTag[A]].runtimeClass){ c => if(value == null || value.isEmpty) default
if(c == classOf[Boolean]) value.toBoolean else convertType(value).asInstanceOf[A]
else if(c == classOf[Int]) value.toInt }
else value
} 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
}
}

View File

@@ -50,6 +50,7 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version. * The history of versions. A head of this sequence is the current BitBucket version.
*/ */
val versions = Seq( val versions = Seq(
Version(1, 11),
Version(1, 10), Version(1, 10),
Version(1, 9), Version(1, 9),
Version(1, 8), Version(1, 8),

View File

@@ -9,7 +9,7 @@ import org.slf4j.LoggerFactory
import javax.servlet.ServletConfig import javax.servlet.ServletConfig
import javax.servlet.ServletContext import javax.servlet.ServletContext
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import util.{Keys, JGitUtil, Directory} import util.{StringUtil, Keys, JGitUtil, Directory}
import util.ControlUtil._ import util.ControlUtil._
import util.Implicits._ import util.Implicits._
import service._ import service._
@@ -50,10 +50,10 @@ class GitRepositoryServlet extends GitServlet {
} }
class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] { class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory]) private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory])
override def create(request: HttpServletRequest, db: Repository): ReceivePack = { override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
val receivePack = new ReceivePack(db) val receivePack = new ReceivePack(db)
val pusher = request.getAttribute(Keys.Request.UserName).asInstanceOf[String] val pusher = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
@@ -64,13 +64,11 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
defining(request.paths){ paths => defining(request.paths){ paths =>
val owner = paths(1) val owner = paths(1)
val repository = paths(2).replaceFirst("\\.git$", "") val repository = paths(2).replaceFirst("\\.git$", "")
val baseURL = request.getRequestURL.toString.replaceFirst("/git/.*", "")
logger.debug("repository:" + owner + "/" + repository) logger.debug("repository:" + owner + "/" + repository)
logger.debug("baseURL:" + baseURL)
if(!repository.endsWith(".wiki")){ if(!repository.endsWith(".wiki")){
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseURL)) receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseUrl(request)))
} }
receivePack receivePack
} }
@@ -79,7 +77,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String, pusher: String, baseURL: String) extends PostReceiveHook class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String) extends PostReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService { with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook]) private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
@@ -143,12 +141,20 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseURL:
} }
} }
// close issues
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch
if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){
git.log.addRange(command.getOldId, command.getNewId).call.asScala.foreach { commit =>
closeIssuesFromMessage(commit.getFullMessage, pusher, owner, repository)
}
}
// call web hook // call web hook
getWebHookURLs(owner, repository) match { getWebHookURLs(owner, repository) match {
case webHookURLs if(webHookURLs.nonEmpty) => case webHookURLs if(webHookURLs.nonEmpty) =>
for(pusherAccount <- getAccountByUserName(pusher); for(pusherAccount <- getAccountByUserName(pusher);
ownerAccount <- getAccountByUserName(owner); ownerAccount <- getAccountByUserName(owner);
repositoryInfo <- getRepository(owner, repository, baseURL)){ repositoryInfo <- getRepository(owner, repository, baseUrl)){
callWebHook(owner, repository, webHookURLs, callWebHook(owner, repository, webHookURLs,
WebHookPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount)) WebHookPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount))
} }
@@ -167,8 +173,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseURL:
} }
private def createIssueComment(commit: CommitInfo) = { private def createIssueComment(commit: CommitInfo) = {
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData => StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
val issueId = matchData.group(2)
if(getIssue(owner, repository, issueId).isDefined){ if(getIssue(owner, repository, issueId).isDefined){
getAccountByMailAddress(commit.mailAddress).foreach { account => getAccountByMailAddress(commit.mailAddress).foreach { account =>
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit") 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) = private def updatePullRequests(branch: String) =
getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq => getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq =>
if(getRepository(pullreq.userName, pullreq.repositoryName, baseURL).isDefined){ if(getRepository(pullreq.userName, pullreq.repositoryName, baseUrl).isDefined){
using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName))){ git => using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName))){ git =>
git.fetch git.fetch
.setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString) .setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString)

View File

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

View File

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

View File

@@ -128,7 +128,7 @@ object JGitUtil {
using(Git.open(getRepositoryDir(owner, repository))){ git => using(Git.open(getRepositoryDir(owner, repository))){ git =>
try { try {
// get commit count // get commit count
val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(1000).sum val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10000).sum
RepositoryInfo( RepositoryInfo(
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", owner, repository, s"${baseUrl}/git/${owner}/${repository}.git",

View File

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

View File

@@ -31,7 +31,7 @@ object StringUtil {
/** /**
* Make string from byte array. Character encoding is detected automatically by [[util.StringUtil.detectEncoding]]. * Make string from byte array. Character encoding is detected automatically by [[util.StringUtil.detectEncoding]].
* And if given bytes contains UTF-8 BOM, it's removed from returned string.. * And if given bytes contains UTF-8 BOM, it's removed from returned string.
*/ */
def convertFromByteArray(content: Array[Byte]): String = def convertFromByteArray(content: Array[Byte]): String =
IOUtils.toString(new BOMInputStream(new java.io.ByteArrayInputStream(content)), detectEncoding(content)) IOUtils.toString(new BOMInputStream(new java.io.ByteArrayInputStream(content)), detectEncoding(content))
@@ -45,4 +45,23 @@ object StringUtil {
case e => e 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 tag = s"h${node.getLevel}"
val headerTextString = printChildrenToString(node) val headerTextString = printChildrenToString(node)
val anchorName = GitBucketHtmlSerializer.generateAnchorName(headerTextString) val anchorName = GitBucketHtmlSerializer.generateAnchorName(headerTextString)
printer.print(s"<$tag>") printer.print(s"""<$tag class="markdown-head">""")
printer.print(s"""<a class="anchor" name="$anchorName" href="#$anchorName"></a>""") printer.print(s"""<a class="markdown-anchor-link" href="#$anchorName"></a>""")
printer.print(s"""<a class="markdown-anchor" name="$anchorName"></a>""")
visitChildren(node) visitChildren(node)
printer.print(s"</$tag>") printer.print(s"</$tag>")
} }
@@ -142,12 +143,10 @@ object GitBucketHtmlSerializer {
private val Whitespace = "[\\s]".r private val Whitespace = "[\\s]".r
private val SpecialChars = "[^\\w-]".r
def generateAnchorName(text: String): String = { def generateAnchorName(text: String): String = {
val noWhitespace = Whitespace.replaceAllIn(text, "-") val noWhitespace = Whitespace.replaceAllIn(text, "-")
val normalized = Normalizer.normalize(noWhitespace, Normalizer.Form.NFD) val normalized = Normalizer.normalize(noWhitespace, Normalizer.Form.NFD)
val noSpecialChars = SpecialChars.replaceAllIn(normalized, "") val noSpecialChars = StringUtil.urlEncode(normalized)
noSpecialChars.toLowerCase(Locale.ENGLISH) noSpecialChars.toLowerCase(Locale.ENGLISH)
} }
} }

View File

@@ -3,205 +3,220 @@
@import util.Directory._ @import util.Directory._
@import view.helpers._ @import view.helpers._
@html.main("System Settings"){ @html.main("System Settings"){
@menu("system"){ @menu("system"){
@helper.html.information(info) @helper.html.information(info)
<form action="@path/admin/system" method="POST" validate="true"> <form action="@path/admin/system" method="POST" validate="true">
<div class="box"> <div class="box">
<div class="box-header">System Settings</div> <div class="box-header">System Settings</div>
<div class="box-content"> <div class="box-content">
<!--====================================================================--> <!--====================================================================-->
<!-- GITBUCKET_HOME --> <!-- GITBUCKET_HOME -->
<!--====================================================================--> <!--====================================================================-->
<label class="strong">GITBUCKET_HOME</label> <label class="strong">GITBUCKET_HOME</label>
@GitBucketHome @GitBucketHome
<!--====================================================================--> <!--====================================================================-->
<!-- Account registration --> <!-- Base URL -->
<!--====================================================================--> <!--====================================================================-->
<hr> <hr>
<label class="strong">Account registration</label> <label><span class="strong">Base URL</span> (e.g. <code>http://example.com/gitbucket</code>)</label>
<fieldset> <fieldset>
<label class="radio"> <div class="controls">
<input type="radio" name="allowAccountRegistration" value="true"@if(settings.allowAccountRegistration){ checked}> <input type="text" name="baseUrl" id="baseUrl" style="width: 400px" value="@settings.baseUrl"/>
<span class="strong">Allow</span> - Users can create accounts by themselves. </div>
</label> </fieldset>
<label class="radio"> <p>
<input type="radio" name="allowAccountRegistration" value="false"@if(!settings.allowAccountRegistration){ checked}> The base URL is used for redirect, notification email, git repository URL box and more.
<span class="strong">Deny</span> - Only administrators can create accounts. If the base URL is empty, GitBucket generates URL from request information.
</label> You can use this property to adjust URL difference between the reverse proxy and GitBucket.
</fieldset> </p>
<!--====================================================================--> <!--====================================================================-->
<!-- Services --> <!-- Account registration -->
<!--====================================================================--> <!--====================================================================-->
<hr> <hr>
<label class="strong">Services</label> <label class="strong">Account registration</label>
<fieldset> <fieldset>
<label class="checkbox"> <label class="radio">
<input type="checkbox" name="gravatar"@if(settings.gravatar){ checked}/> <input type="radio" name="allowAccountRegistration" value="true"@if(settings.allowAccountRegistration){ checked}>
Use Gravatar for Profile-Images <span class="strong">Allow</span> - Users can create accounts by themselves.
</label> </label>
</fieldset> <label class="radio">
<!--====================================================================--> <input type="radio" name="allowAccountRegistration" value="false"@if(!settings.allowAccountRegistration){ checked}>
<!-- Authentication --> <span class="strong">Deny</span> - Only administrators can create accounts.
<!--====================================================================--> </label>
<hr> </fieldset>
<label class="strong">Authentication</label> <!--====================================================================-->
<fieldset> <!-- Services -->
<label class="checkbox"> <!--====================================================================-->
<input type="checkbox" id="ldapAuthentication" name="ldapAuthentication"@if(settings.ldap){ checked}/> <hr>
LDAP <label class="strong">Services</label>
</label> <fieldset>
</fieldset> <label class="checkbox">
<div class="form-horizontal ldap"> <input type="checkbox" name="gravatar"@if(settings.gravatar){ checked}/>
<div class="control-group"> Use Gravatar for Profile-Images
<label class="control-label" for="ldapHost">LDAP Host</label> </label>
<div class="controls"> </fieldset>
<input type="text" id="ldapHost" name="ldap.host" value="@settings.ldap.map(_.host)"/> <!--====================================================================-->
<span id="error-ldap_host" class="error"></span> <!-- Authentication -->
</div> <!--====================================================================-->
</div> <hr>
<div class="control-group"> <label class="strong">Authentication</label>
<label class="control-label" for="ldapPort">LDAP Port</label> <fieldset>
<div class="controls"> <label class="checkbox">
<input type="text" id="ldapPort" name="ldap.port" class="input-mini" value="@settings.ldap.map(_.port)"/> <input type="checkbox" id="ldapAuthentication" name="ldapAuthentication"@if(settings.ldap){ checked}/>
<span id="error-ldap_port" class="error"></span> LDAP
</div> </label>
</div> </fieldset>
<div class="control-group"> <div class="form-horizontal ldap">
<label class="control-label" for="ldapBindDN">Bind DN</label> <div class="control-group">
<div class="controls"> <label class="control-label" for="ldapHost">LDAP Host</label>
<input type="text" id="ldapBindDN" name="ldap.bindDN" value="@settings.ldap.map(_.bindDN)"/> <div class="controls">
<span id="error-ldap_bindDN" class="error"></span> <input type="text" id="ldapHost" name="ldap.host" value="@settings.ldap.map(_.host)"/>
</div> <span id="error-ldap_host" class="error"></span>
</div> </div>
<div class="control-group">
<label class="control-label" for="ldapBindPassword">Bind Password</label>
<div class="controls">
<input type="password" id="ldapBindPassword" name="ldap.bindPassword" value="@settings.ldap.map(_.bindPassword)"/>
<span id="error-ldap_bindPassword" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapBaseDN">Base DN</label>
<div class="controls">
<input type="text" id="ldapBaseDN" name="ldap.baseDN" value="@settings.ldap.map(_.baseDN)"/>
<span id="error-ldap_baseDN" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapUserNameAttribute">User name attribute</label>
<div class="controls">
<input type="text" id="ldapUserNameAttribute" name="ldap.userNameAttribute" value="@settings.ldap.map(_.userNameAttribute)"/>
<span id="error-ldap_userNameAttribute" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapAdditionalFilterCondition">Additional filter condition</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>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapFullNameAttribute">Full name attribute</label>
<div class="controls">
<input type="text" id="ldapFullNameAttribute" name="ldap.fullNameAttribute" value="@settings.ldap.map(_.fullNameAttribute)"/>
<span id="error-ldap_fullNameAttribute" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapMailAttribute">Mail address attribute</label>
<div class="controls">
<input type="text" id="ldapMailAttribute" name="ldap.mailAttribute" value="@settings.ldap.map(_.mailAttribute)"/>
<span id="error-ldap_mailAttribute" class="error"></span>
</div>
</div>
<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
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" name="ldap.tls"@if(settings.ldap.flatMap(_.tls).getOrElse(false)){ checked}/> Enable TLS
</label>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapBindDN">Keystore</label>
<div class="controls">
<input type="text" id="ldapKeystore" name="ldap.keystore" value="@settings.ldap.map(_.keystore)"/>
<span id="error-ldap_keystore" class="error"></span>
</div>
</div>
</div> </div>
<!--====================================================================--> <div class="control-group">
<!-- Notification email --> <label class="control-label" for="ldapPort">LDAP Port</label>
<!--====================================================================--> <div class="controls">
<hr> <input type="text" id="ldapPort" name="ldap.port" class="input-mini" value="@settings.ldap.map(_.port)"/>
<label class="strong">Notification email</label> <span id="error-ldap_port" class="error"></span>
<fieldset> </div>
<label class="checkbox">
<input type="checkbox" id="notification" name="notification"@if(settings.notification){ checked}/>
Send notifications
</label>
</fieldset>
<div class="form-horizontal notification">
<div class="control-group">
<label class="control-label" for="smtpHost">SMTP Host</label>
<div class="controls">
<input type="text" id="smtpHost" name="smtp.host" value="@settings.smtp.map(_.host)"/>
<span id="error-smtp_host" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="smtpPort">SMTP Port</label>
<div class="controls">
<input type="text" id="smtpPort" name="smtp.port" class="input-mini" value="@settings.smtp.map(_.port)"/>
<span id="error-smtp_port" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="smtpUser">SMTP User</label>
<div class="controls">
<input type="text" id="smtpUser" name="smtp.user" value="@settings.smtp.map(_.user)"/>
</div>
</div>
<div class="control-group">
<label class="control-label" for="smtpPassword">SMTP Password</label>
<div class="controls">
<input type="password" id="smtpPassword" name="smtp.password" value="@settings.smtp.map(_.password)"/>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" name="smtp.ssl"@if(settings.smtp.flatMap(_.ssl).getOrElse(false)){ checked}/> Enable SSL
</label>
</div>
</div>
<div class="control-group">
<label class="control-label" for="fromAddress">FROM Address</label>
<div class="controls">
<input type="text" id="fromAddress" name="smtp.fromAddress" value="@settings.smtp.map(_.fromAddress)"/>
</div>
</div>
<div class="control-group">
<label class="control-label" for="fromName">FROM Name</label>
<div class="controls">
<input type="text" id="fromName" name="smtp.fromName" value="@settings.smtp.map(_.fromName)"/>
</div>
</div>
</div> </div>
<div class="control-group">
<label class="control-label" for="ldapBindDN">Bind DN</label>
<div class="controls">
<input type="text" id="ldapBindDN" name="ldap.bindDN" value="@settings.ldap.map(_.bindDN)"/>
<span id="error-ldap_bindDN" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapBindPassword">Bind Password</label>
<div class="controls">
<input type="password" id="ldapBindPassword" name="ldap.bindPassword" value="@settings.ldap.map(_.bindPassword)"/>
<span id="error-ldap_bindPassword" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapBaseDN">Base DN</label>
<div class="controls">
<input type="text" id="ldapBaseDN" name="ldap.baseDN" value="@settings.ldap.map(_.baseDN)"/>
<span id="error-ldap_baseDN" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapUserNameAttribute">User name attribute</label>
<div class="controls">
<input type="text" id="ldapUserNameAttribute" name="ldap.userNameAttribute" value="@settings.ldap.map(_.userNameAttribute)"/>
<span id="error-ldap_userNameAttribute" class="error"></span>
</div>
</div>
<div class="control-group">
<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>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapFullNameAttribute">Full name attribute</label>
<div class="controls">
<input type="text" id="ldapFullNameAttribute" name="ldap.fullNameAttribute" value="@settings.ldap.map(_.fullNameAttribute)"/>
<span id="error-ldap_fullNameAttribute" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapMailAttribute">Mail address attribute</label>
<div class="controls">
<input type="text" id="ldapMailAttribute" name="ldap.mailAttribute" value="@settings.ldap.map(_.mailAttribute)"/>
<span id="error-ldap_mailAttribute" class="error"></span>
</div>
</div>
<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
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" name="ldap.tls"@if(settings.ldap.flatMap(_.tls).getOrElse(false)){ checked}/> Enable TLS
</label>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapBindDN">Keystore</label>
<div class="controls">
<input type="text" id="ldapKeystore" name="ldap.keystore" value="@settings.ldap.map(_.keystore)"/>
<span id="error-ldap_keystore" class="error"></span>
</div>
</div>
</div>
<!--====================================================================-->
<!-- Notification email -->
<!--====================================================================-->
<hr>
<label class="strong">Notification email</label>
<fieldset>
<label class="checkbox">
<input type="checkbox" id="notification" name="notification"@if(settings.notification){ checked}/>
Send notifications
</label>
</fieldset>
<div class="form-horizontal notification">
<div class="control-group">
<label class="control-label" for="smtpHost">SMTP Host</label>
<div class="controls">
<input type="text" id="smtpHost" name="smtp.host" value="@settings.smtp.map(_.host)"/>
<span id="error-smtp_host" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="smtpPort">SMTP Port</label>
<div class="controls">
<input type="text" id="smtpPort" name="smtp.port" class="input-mini" value="@settings.smtp.map(_.port)"/>
<span id="error-smtp_port" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="smtpUser">SMTP User</label>
<div class="controls">
<input type="text" id="smtpUser" name="smtp.user" value="@settings.smtp.map(_.user)"/>
</div>
</div>
<div class="control-group">
<label class="control-label" for="smtpPassword">SMTP Password</label>
<div class="controls">
<input type="password" id="smtpPassword" name="smtp.password" value="@settings.smtp.map(_.password)"/>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" name="smtp.ssl"@if(settings.smtp.flatMap(_.ssl).getOrElse(false)){ checked}/> Enable SSL
</label>
</div>
</div>
<div class="control-group">
<label class="control-label" for="fromAddress">FROM Address</label>
<div class="controls">
<input type="text" id="fromAddress" name="smtp.fromAddress" value="@settings.smtp.map(_.fromAddress)"/>
</div>
</div>
<div class="control-group">
<label class="control-label" for="fromName">FROM Name</label>
<div class="controls">
<input type="text" id="fromName" name="smtp.fromName" value="@settings.smtp.map(_.fromName)"/>
</div>
</div>
</div>
</div> </div>
</div> </div>
<fieldset> <fieldset>
<input type="submit" class="btn btn-success" value="Apply changes"/> <input type="submit" class="btn btn-success" value="Apply changes"/>
</fieldset> </fieldset>
</form> </form>
} }
} }
<script> <script>
$(function(){ $(function(){
@@ -213,4 +228,4 @@ $(function(){
$('.ldap input').prop('disabled', !$(this).prop('checked')); $('.ldap input').prop('disabled', !$(this).prop('checked'));
}).change(); }).change();
}); });
</script> </script>

View File

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

View File

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

View File

@@ -11,10 +11,16 @@
<div class="box issue-comment-box" id="comment-@comment.commentId"> <div class="box issue-comment-box" id="comment-@comment.commentId">
<div class="box-header-small"> <div class="box-header-small">
<i class="icon-comment"></i> <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"> <span class="pull-right">
@datetime(comment.registeredDate) @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-pencil"></i></a>&nbsp;
<a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle"></i></a> <a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle"></i></a>
} }
@@ -27,7 +33,13 @@
@markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true) @markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true)
} }
} else { } else {
@markdown(comment.content, repository, false, true) @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>
</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> <a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a>
} else { } 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><!--/.nav-collapse -->
</div> </div>
@@ -76,6 +76,7 @@
$('#search').submit(function(){ $('#search').submit(function(){
return $.trim($(this).find('input[name=query]').val()) != ''; return $.trim($(this).find('input[name=query]').val()) != '';
}); });
$('#signin').attr('href', '@path/signin?redirect=' + encodeURIComponent(location.pathname + location.search + location.hash));
}); });
</script> </script>
</body> </body>

View File

@@ -30,7 +30,7 @@
<fieldset class="margin"> <fieldset class="margin">
<label class="radio"> <label class="radio">
<input type="radio" name="isPrivate" value="false" checked> <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> <div>
<span>All users and guests can read this repository.</span> <span>All users and guests can read this repository.</span>
</div> </div>
@@ -39,7 +39,7 @@
<fieldset> <fieldset>
<label class="radio"> <label class="radio">
<input type="radio" name="isPrivate" value="true"> <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> <div>
<span>Only collaborators can read this repository.</span> <span>Only collaborators can read this repository.</span>
</div> </div>

View File

@@ -3,7 +3,7 @@
pathList: List[String], pathList: List[String],
latestCommit: util.JGitUtil.CommitInfo, latestCommit: util.JGitUtil.CommitInfo,
files: List[util.JGitUtil.FileInfo], 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 context._
@import view.helpers._ @import view.helpers._
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { @html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@@ -12,7 +12,7 @@
<div class="head"> <div class="head">
<div class="pull-right"> <div class="pull-right">
@defining(repository.commitCount){ commitCount => @defining(repository.commitCount){ commitCount =>
<a href="@url(repository)/commits/@encodeRefName(branch)">@if(commitCount > 1000){ @commitCount+ } else { @commitCount } @plural(commitCount, "commit")</a>&nbsp; <a href="@url(repository)/commits/@encodeRefName(branch)">@if(commitCount > 10000){ @commitCount+ } else { @commitCount } @plural(commitCount, "commit")</a>&nbsp;
} }
</div> </div>
<a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> / <a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> /
@@ -77,9 +77,9 @@
</table> </table>
</div> </div>
@readme.map { content => @readme.map { case(file, content) =>
<div id="readme" class="box"> <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 class="box-content markdown-body">@markdown(content, repository, false, false)</div>
</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; border-top-right-radius: 4px;
-moz-border-radius-topright: 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(); $('img[data-toggle=tooltip]').tooltip();
$('a[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 // syntax highlighting by google-code-prettify
prettyPrint(); prettyPrint();
}); });

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ import twirl.api.Html
class AvatarImageProviderSpec extends Specification { class AvatarImageProviderSpec extends Specification {
implicit val context = app.Context("", None, "", null) implicit val context = app.Context("", None, null)
"getAvatarImageHtml" should { "getAvatarImageHtml" should {
"show Gravatar image for no image account if gravatar integration is enabled" in { "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) = private def createSystemSettings(useGravatar: Boolean) =
SystemSettings( SystemSettings(
baseUrl = None,
allowAccountRegistration = false, allowAccountRegistration = false,
gravatar = useGravatar, gravatar = useGravatar,
notification = false, notification = false,

View File

@@ -16,13 +16,13 @@ class GitBucketHtmlSerializerSpec extends Specification {
"normalize characters with diacritics" in { "normalize characters with diacritics" in {
val before = "Dónde estará mi vida" val before = "Dónde estará mi vida"
val after = generateAnchorName(before) val after = generateAnchorName(before)
after mustEqual "donde-estara-mi-vida" after mustEqual "do%cc%81nde-estara%cc%81-mi-vida"
} }
"omit special characters" in { "omit special characters" in {
val before = "foo!bar@baz>9000" val before = "foo!bar@baz>9000"
val after = generateAnchorName(before) val after = generateAnchorName(before)
after mustEqual "foobarbaz9000" after mustEqual "foo%21bar%40baz%3e9000"
} }
} }
} }