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
17
README.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 &
|
||||
|
||||
242
etc/icons.svg
@@ -25,17 +25,17 @@
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.4"
|
||||
inkscape:cx="629.30023"
|
||||
inkscape:cy="281.44758"
|
||||
inkscape:cx="450.21999"
|
||||
inkscape:cy="97.51519"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1-9"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="705"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-height="706"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-global="true"
|
||||
inkscape:snap-global="false"
|
||||
inkscape:snap-grids="false"
|
||||
inkscape:snap-page="false"
|
||||
inkscape:snap-bbox="true"
|
||||
@@ -746,6 +746,238 @@
|
||||
d="m 937.41093,1044.4944 0,30.6797 -28.50183,0 0,41.2377 28.50183,0 0,27.1288 41.19033,0 0,-27.1288 29.35404,0 0,-41.2377 -29.35404,0 0,-30.6797 -41.19033,0 z"
|
||||
id="rect2995-0-2-7-7"
|
||||
inkscape:connector-curvature="0" />
|
||||
<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 |
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)))
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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())),
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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))
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
@import util.Directory._
|
||||
@import view.helpers._
|
||||
@html.main("System Settings"){
|
||||
@menu("system"){
|
||||
@helper.html.information(info)
|
||||
<form action="@path/admin/system" method="POST" validate="true">
|
||||
@menu("system"){
|
||||
@helper.html.information(info)
|
||||
<form action="@path/admin/system" method="POST" validate="true">
|
||||
<div class="box">
|
||||
<div class="box-header">System Settings</div>
|
||||
<div class="box-content">
|
||||
@@ -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>
|
||||
@@ -200,8 +215,8 @@
|
||||
<fieldset>
|
||||
<input type="submit" class="btn btn-success" value="Apply changes"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
}
|
||||
</form>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
|
||||
@@ -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}">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
<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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"> </i> Public</span><br>
|
||||
<span class="strong"><img src="@assets/common/images/repo_public.png"/> </i> 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"> </i> Private</span><br>
|
||||
<span class="strong"><img src="@assets/common/images/repo_private.png"/> </i> Private</span><br>
|
||||
<div>
|
||||
<span>Only collaborators can read this repository.</span>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
<a href="@url(repository)/commits/@encodeRefName(branch)">@if(commitCount > 10000){ @commitCount+ } else { @commitCount } @plural(commitCount, "commit")</a>
|
||||
}
|
||||
</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>
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
BIN
src/main/webapp/assets/common/images/link.png
Normal file
|
After Width: | Height: | Size: 343 B |
BIN
src/main/webapp/assets/common/images/repo_fork.png
Normal file
|
After Width: | Height: | Size: 285 B |
BIN
src/main/webapp/assets/common/images/repo_fork_lg.png
Normal file
|
After Width: | Height: | Size: 442 B |
BIN
src/main/webapp/assets/common/images/repo_private.png
Normal file
|
After Width: | Height: | Size: 280 B |
BIN
src/main/webapp/assets/common/images/repo_private_lg.png
Normal file
|
After Width: | Height: | Size: 420 B |
BIN
src/main/webapp/assets/common/images/repo_public.png
Normal file
|
After Width: | Height: | Size: 247 B |
BIN
src/main/webapp/assets/common/images/repo_public_lg.png
Normal file
|
After Width: | Height: | Size: 352 B |
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||