Merge branch 'master' into closing-issues-via-commit-messages

Conflicts:
	src/main/scala/app/PullRequestsController.scala
	src/main/scala/servlet/GitRepositoryServlet.scala
This commit is contained in:
odz
2014-02-24 23:48:05 +09:00
71 changed files with 1772 additions and 569 deletions

1
.gitignore vendored
View File

@@ -14,6 +14,7 @@ project/plugins/project/
.classpath .classpath
.project .project
.cache .cache
.settings
# IntelliJ specific # IntelliJ specific
.idea/ .idea/

View File

@@ -37,23 +37,64 @@ Installation
The default administrator account is **root** and password is **root**. The default administrator account is **root** and password is **root**.
or you can start GitBucket by ```java -jar gitbucket.war``` without servlet container. In this case, GitBucket URL is **http://[hostname]:8080/**. You can specify following options. or you can start GitBucket by `java -jar gitbucket.war` without servlet container. In this case, GitBucket URL is **http://[hostname]:8080/**. You can specify following options.
- --port=[NUMBER] - --port=[NUMBER]
- --prefix=[CONTEXTPATH] - --prefix=[CONTEXTPATH]
- --host=[HOSTNAME] - --host=[HOSTNAME]
- --https=true - --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. 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.
For Installation on Windows Server with IIS see [this wiki page](https://github.com/takezoe/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo) For Installation on Windows Server with IIS see [this wiki page](https://github.com/takezoe/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo)
### Mac OS X
On OS X, copy the [gitbucket.plist](https://raw.github.com/takezoe/gitbucket/master/contrib/macosx/gitbucket.plist) file to `~/Library/LaunchAgents/`
Run the following commands in `Terminal` to
- start gitbucket: `launchctl load ~/Library/LaunchAgents/gitbucket.plist`
- stop gitbucket: `launchctl unload ~/Library/LaunchAgents/gitbucket.plist`
Release Notes Release Notes
-------- --------
### 1.11 - End of Feb 2014
- Base URL for redirect, notification and repository URL box is configurable
- 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 performance improvement
- Fix some bugs
### 1.9 - 28 Dec 2013
- Display GITBUCKET_HOME on the system settings page
- Fix some bugs
### 1.8 - 30 Nov 2013
- Add user and group deletion
- Improve pull request performance
- Pull request synchronization (when source repository is updated after pull request, it's applied to the pull request)
- LDAP StartTLS support
- Enable hard wrapping in Markdown
- Add new some options to specify the data directory. See details in [Wiki](https://github.com/takezoe/gitbucket/wiki/DirectoryStructure).
- Fix some bugs
### 1.7 - 26 Oct 2013 ### 1.7 - 26 Oct 2013
- Support working on Java6 in embedded Jetty mode - Support working on Java6 in embedded Jetty mode
- Add ```--host``` option to bind specified host name in embedded Jetty mode - Add `--host` option to bind specified host name in embedded Jetty mode
- Add ```--https=true``` option to use https in embedded Jetty mode - Add `--https=true` option to force https scheme when using embedded Jetty mode at the back of https proxy
- Add full name as user property - Add full name as user property
- Change link color for absent Wiki pages - Change link color for absent Wiki pages
- Add ZIP download button to the repository viewer tab - Add ZIP download button to the repository viewer tab

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>gitbucket</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/java</string>
<string>-Dmail.smtp.starttls.enable=true</string>
<string>-jar</string>
<string>gitbucket.war</string>
<string>--host=127.0.0.1</string>
<string>--port=8080</string>
<string>--https=true</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>

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

@@ -1,8 +1,6 @@
import sbt._ import sbt._
import Keys._ import Keys._
import org.scalatra.sbt._ import org.scalatra.sbt._
import org.scalatra.sbt.PluginKeys._
import sbt.ScalaVersion
import twirl.sbt.TwirlPlugin._ import twirl.sbt.TwirlPlugin._
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
@@ -25,14 +23,14 @@ object MyBuild extends Build {
Classpaths.typesafeReleases, Classpaths.typesafeReleases,
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/" "amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
), ),
scalacOptions := Seq("-deprecation"), scalacOptions := Seq("-deprecation", "-language:postfixOps"),
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r", "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r",
"org.scalatra" %% "scalatra" % ScalatraVersion, "org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test", "org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
"org.scalatra" %% "scalatra-json" % ScalatraVersion, "org.scalatra" %% "scalatra-json" % ScalatraVersion,
"org.json4s" %% "json4s-jackson" % "3.2.5", "org.json4s" %% "json4s-jackson" % "3.2.5",
"jp.sf.amateras" %% "scalatra-forms" % "0.0.8", "jp.sf.amateras" %% "scalatra-forms" % "0.0.11",
"commons-io" % "commons-io" % "2.4", "commons-io" % "commons-io" % "2.4",
"org.pegdown" % "pegdown" % "1.4.1", "org.pegdown" % "pegdown" % "1.4.1",
"org.apache.commons" % "commons-compress" % "1.5", "org.apache.commons" % "commons-compress" % "1.5",
@@ -43,7 +41,7 @@ object MyBuild extends Build {
"com.h2database" % "h2" % "1.3.173", "com.h2database" % "h2" % "1.3.173",
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime", "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided", "org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar")), "org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"),
"junit" % "junit" % "4.11" % "test" "junit" % "junit" % "4.11" % "test"
), ),
EclipseKeys.withSource := true, EclipseKeys.withSource := true,

View File

@@ -53,6 +53,9 @@ public class JettyLauncher {
context.setDescriptor(location.toExternalForm() + "/WEB-INF/web.xml"); context.setDescriptor(location.toExternalForm() + "/WEB-INF/web.xml");
context.setServer(server); context.setServer(server);
context.setWar(location.toExternalForm()); context.setWar(location.toExternalForm());
if (forceHttps) {
context.setInitParameter("org.scalatra.ForceHttps", "true");
}
server.setHandler(context); server.setHandler(context);
server.start(); server.start();

View File

@@ -17,7 +17,6 @@ class ScalatraBootstrap extends LifeCycle {
context.mount(new IndexController, "/") context.mount(new IndexController, "/")
context.mount(new SearchController, "/") context.mount(new SearchController, "/")
context.mount(new FileUploadController, "/upload") context.mount(new FileUploadController, "/upload")
context.mount(new SignInController, "/*")
context.mount(new DashboardController, "/*") context.mount(new DashboardController, "/*")
context.mount(new UserManagementController, "/*") context.mount(new UserManagementController, "/*")
context.mount(new SystemSettingsController, "/*") context.mount(new SystemSettingsController, "/*")

View File

@@ -9,12 +9,10 @@ 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 with FlashMapSupport {
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,7 @@ 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 Validations with SystemSettingsService {
implicit val jsonFormats = DefaultFormats implicit val jsonFormats = DefaultFormats
@@ -58,11 +57,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 +102,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))) val currentUrl = baseUrl + defining(request.getQueryString){ queryString =>
request.getRequestURI.substring(request.getContextPath.length) + (if(queryString != null) "?" + queryString else "")
}
session.setAttribute(Keys.Session.Redirect, currentUrl)
org.scalatra.Unauthorized(redirect("/signin"))
} }
} }
} }
protected def baseUrl = defining(request.getRequestURL.toString){ url => protected def baseUrl = loadSystemSettings().baseUrl.getOrElse {
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length)) defining(request.getRequestURL.toString){ url =>
} url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
}
}.replaceFirst("/$", "")
} }
/** /**
* 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

@@ -113,54 +113,62 @@ trait CreateRepositoryControllerBase extends ControllerBase {
val loginUserName = loginAccount.userName val loginUserName = loginAccount.userName
LockUtil.lock(s"${loginUserName}/${repository.name}/create"){ LockUtil.lock(s"${loginUserName}/${repository.name}/create"){
if(getRepository(loginUserName, repository.name, baseUrl).isEmpty){ if(repository.owner == loginUserName){
// Insert to the database at first // redirect to the repository
val originUserName = repository.repository.originUserName.getOrElse(repository.owner) redirect(s"/${repository.owner}/${repository.name}")
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name) } else {
getForkedRepositories(repository.owner, repository.name).find(_._1 == loginUserName).map { case (owner, name) =>
// redirect to the repository
redirect(s"/${owner}/${name}")
} getOrElse {
// Insert to the database at first
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
createRepository( createRepository(
repositoryName = repository.name, repositoryName = repository.name,
userName = loginUserName, userName = loginUserName,
description = repository.repository.description, description = repository.repository.description,
isPrivate = repository.repository.isPrivate, isPrivate = repository.repository.isPrivate,
originRepositoryName = Some(originRepositoryName), originRepositoryName = Some(originRepositoryName),
originUserName = Some(originUserName), originUserName = Some(originUserName),
parentRepositoryName = Some(repository.name), parentRepositoryName = Some(repository.name),
parentUserName = Some(repository.owner) parentUserName = Some(repository.owner)
) )
// Insert default labels // Insert default labels
insertDefaultLabels(loginUserName, repository.name) insertDefaultLabels(loginUserName, repository.name)
// clone repository actually // clone repository actually
JGitUtil.cloneRepository( JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name), getRepositoryDir(repository.owner, repository.name),
getRepositoryDir(loginUserName, repository.name)) getRepositoryDir(loginUserName, repository.name))
// Create Wiki repository // Create Wiki repository
JGitUtil.cloneRepository( JGitUtil.cloneRepository(
getWikiRepositoryDir(repository.owner, repository.name), getWikiRepositoryDir(repository.owner, repository.name),
getWikiRepositoryDir(loginUserName, repository.name)) getWikiRepositoryDir(loginUserName, repository.name))
// insert commit id // insert commit id
using(Git.open(getRepositoryDir(loginUserName, repository.name))){ git => using(Git.open(getRepositoryDir(loginUserName, repository.name))){ git =>
JGitUtil.getRepositoryInfo(loginUserName, repository.name, baseUrl).branchList.foreach { branch => JGitUtil.getRepositoryInfo(loginUserName, repository.name, baseUrl).branchList.foreach { branch =>
JGitUtil.getCommitLog(git, branch) match { JGitUtil.getCommitLog(git, branch) match {
case Right((commits, _)) => commits.foreach { commit => case Right((commits, _)) => commits.foreach { commit =>
if(!existsCommitId(loginUserName, repository.name, commit.id)){ if(!existsCommitId(loginUserName, repository.name, commit.id)){
insertCommitId(loginUserName, repository.name, commit.id) insertCommitId(loginUserName, repository.name, commit.id)
}
} }
case Left(_) => ???
} }
case Left(_) => ???
} }
} }
}
// Record activity // Record activity
recordForkActivity(repository.owner, repository.name, loginUserName) recordForkActivity(repository.owner, repository.name, loginUserName)
// redirect to the repository
redirect(s"/${loginUserName}/${repository.name}")
}
} }
// redirect to the repository
redirect("/%s/%s".format(loginUserName, repository.name))
} }
}) })

View File

@@ -1,16 +1,22 @@
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 SystemSettingsService with ActivityService with AccountService with RepositoryService with ActivityService with AccountService with UsersAuthenticator
with UsersAuthenticator
trait IndexControllerBase extends ControllerBase { trait IndexControllerBase extends ControllerBase {
self: RepositoryService with SystemSettingsService with ActivityService with AccountService self: RepositoryService with ActivityService with AccountService with UsersAuthenticator =>
with UsersAuthenticator =>
case class SignInForm(userName: String, password: String)
val form = mapping(
"userName" -> trim(label("Username", text(required))),
"password" -> trim(label("Password", text(required)))
)(SignInForm.apply)
get("/"){ get("/"){
val loginAccount = context.loginAccount val loginAccount = context.loginAccount
@@ -22,6 +28,44 @@ trait IndexControllerBase extends ControllerBase {
) )
} }
get("/signin"){
val redirect = params.get("redirect")
if(redirect.isDefined && redirect.get.startsWith("/")){
session.setAttribute(Keys.Session.Redirect, redirect.get)
}
html.signin(loadSystemSettings())
}
post("/signin", form){ form =>
authenticate(loadSystemSettings(), form.userName, form.password) match {
case Some(account) => signin(account)
case None => redirect("/signin")
}
}
get("/signout"){
session.invalidate
redirect("/")
}
/**
* Set account information into HttpSession and redirect.
*/
private def signin(account: model.Account) = {
session.setAttribute(Keys.Session.LoginAccount, account)
updateLastLoginDate(account.userName)
session.getAndRemove[String](Keys.Session.Redirect).map { redirectUrl =>
if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){
redirect("/")
} else {
redirect(redirectUrl)
}
}.getOrElse {
redirect("/")
}
}
/** /**
* JSON API for collaborator completion. * JSON API for collaborator completion.
* *

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

@@ -18,26 +18,28 @@ import util.JGitUtil.CommitInfo
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.eclipse.jgit.merge.MergeStrategy import org.eclipse.jgit.merge.MergeStrategy
import org.eclipse.jgit.errors.NoMergeBaseException import org.eclipse.jgit.errors.NoMergeBaseException
import service.WebHookService.WebHookPayload
class PullRequestsController extends PullRequestsControllerBase class PullRequestsController extends PullRequestsControllerBase
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with ActivityService with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
with ReferrerAuthenticator with CollaboratorsAuthenticator with ActivityService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator
trait PullRequestsControllerBase extends ControllerBase { trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with IssuesService with MilestonesService with ActivityService with PullRequestService self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
with ReferrerAuthenticator with CollaboratorsAuthenticator => with ActivityService with PullRequestService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase]) private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
val pullRequestForm = mapping( val pullRequestForm = mapping(
"title" -> trim(label("Title" , text(required, maxlength(100)))), "title" -> trim(label("Title" , text(required, maxlength(100)))),
"content" -> trim(label("Content", optional(text()))), "content" -> trim(label("Content", optional(text()))),
"targetUserName" -> trim(text(required, maxlength(100))), "targetUserName" -> trim(text(required, maxlength(100))),
"targetBranch" -> trim(text(required, maxlength(100))), "targetBranch" -> trim(text(required, maxlength(100))),
"requestUserName" -> trim(text(required, maxlength(100))), "requestUserName" -> trim(text(required, maxlength(100))),
"requestBranch" -> trim(text(required, maxlength(100))), "requestRepositoryName" -> trim(text(required, maxlength(100))),
"commitIdFrom" -> trim(text(required, maxlength(40))), "requestBranch" -> trim(text(required, maxlength(100))),
"commitIdTo" -> trim(text(required, maxlength(40))) "commitIdFrom" -> trim(text(required, maxlength(40))),
"commitIdTo" -> trim(text(required, maxlength(40)))
)(PullRequestForm.apply) )(PullRequestForm.apply)
val mergeForm = mapping( val mergeForm = mapping(
@@ -50,6 +52,7 @@ trait PullRequestsControllerBase extends ControllerBase {
targetUserName: String, targetUserName: String,
targetBranch: String, targetBranch: String,
requestUserName: String, requestUserName: String,
requestRepositoryName: String,
requestBranch: String, requestBranch: String,
commitIdFrom: String, commitIdFrom: String,
commitIdTo: String) commitIdTo: String)
@@ -76,8 +79,10 @@ 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),
(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),
commits, commits,
diffs, diffs,
hasWritePermission(owner, name, context.loginAccount), hasWritePermission(owner, name, context.loginAccount),
@@ -90,7 +95,7 @@ trait PullRequestsControllerBase extends ControllerBase {
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository => ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository =>
params("id").toIntOpt.flatMap{ issueId => params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner val owner = repository.owner
val name = repository.name val name = repository.name
getPullRequest(owner, name, issueId) map { case(issue, pullreq) => getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
pulls.html.mergeguide( pulls.html.mergeguide(
checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId), checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId),
@@ -100,10 +105,25 @@ trait PullRequestsControllerBase extends ControllerBase {
} getOrElse NotFound } getOrElse NotFound
}) })
get("/:owner/:repository/pull/:id/delete/:branchName")(collaboratorsOnly { repository =>
params("id").toIntOpt.map { issueId =>
val branchName = params("branchName")
val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.branchDelete().setBranchNames(branchName).call()
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
}
}
createComment(repository.owner, repository.name, userName, issueId, branchName, "delete_branch")
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} getOrElse NotFound
})
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) => post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
params("id").toIntOpt.flatMap { issueId => params("id").toIntOpt.flatMap { issueId =>
val owner = repository.owner val owner = repository.owner
val name = repository.name val name = repository.name
LockUtil.lock(s"${owner}/${name}/merge"){ LockUtil.lock(s"${owner}/${name}/merge"){
getPullRequest(owner, name, issueId).map { case (issue, pullreq) => getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
using(Git.open(getRepositoryDir(owner, name))) { git => using(Git.open(getRepositoryDir(owner, name))) { git =>
@@ -175,6 +195,15 @@ trait PullRequestsControllerBase extends ControllerBase {
} }
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name) closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
} }
// call web hook
getWebHookURLs(owner, name) match {
case webHookURLs if(webHookURLs.nonEmpty) =>
for(ownerAccount <- getAccountByUserName(owner)){
callWebHook(owner, name, webHookURLs,
WebHookPayload(git, loginAccount, mergeBaseRefName, repository, commits.flatten.toList, ownerAccount))
}
case _ =>
}
// notifications // notifications
Notifier().toNotify(repository, issueId, "merge"){ Notifier().toNotify(repository, issueId, "merge"){
@@ -215,73 +244,85 @@ trait PullRequestsControllerBase extends ControllerBase {
} }
}) })
get("/:owner/:repository/compare/*...*")(referrersOnly { repository => get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat") val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner) val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner) val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
(getRepository(originOwner, repository.name, baseUrl), (for(
getRepository(forkedOwner, repository.name, baseUrl)) match { originRepositoryName <- if(originOwner == forkedOwner){
case (Some(originRepository), Some(forkedRepository)) => { Some(forkedRepository.name)
using( } else {
Git.open(getRepositoryDir(originOwner, repository.name)), forkedRepository.repository.originRepositoryName.orElse {
Git.open(getRepositoryDir(forkedOwner, repository.name)) getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
){ case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
val forkedId = getForkedCommitId(oldGit, newGit,
originOwner, repository.name, originBranch,
forkedOwner, repository.name, forkedBranch)
val oldId = oldGit.getRepository.resolve(forkedId)
val newId = newGit.getRepository.resolve(forkedBranch)
val (commits, diffs) = getRequestCompareInfo(
originOwner, repository.name, oldId.getName,
forkedOwner, repository.name, newId.getName)
pulls.html.compare(
commits,
diffs,
repository.repository.originUserName.map { userName =>
userName :: getForkedRepositories(userName, repository.name)
} getOrElse List(repository.owner),
originBranch,
forkedBranch,
oldId.getName,
newId.getName,
repository,
originRepository,
forkedRepository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
} }
};
originRepository <- getRepository(originOwner, originRepositoryName, baseUrl)
) yield {
using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
){ case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
val forkedId = getForkedCommitId(oldGit, newGit,
originRepository.owner, originRepository.name, originBranch,
forkedRepository.owner, forkedRepository.name, forkedBranch)
val oldId = oldGit.getRepository.resolve(forkedId)
val newId = newGit.getRepository.resolve(forkedBranch)
val (commits, diffs) = getRequestCompareInfo(
originRepository.owner, originRepository.name, oldId.getName,
forkedRepository.owner, forkedRepository.name, newId.getName)
pulls.html.compare(
commits,
diffs,
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
},
originBranch,
forkedBranch,
oldId.getName,
newId.getName,
forkedRepository,
originRepository,
forkedRepository,
hasWritePermission(forkedRepository.owner, forkedRepository.name, context.loginAccount))
} }
case _ => NotFound }) getOrElse NotFound
}
}) })
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { repository => ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat") val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner) val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner) val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
(getRepository(originOwner, repository.name, baseUrl), (for(
getRepository(forkedOwner, repository.name, baseUrl)) match { originRepositoryName <- if(originOwner == forkedOwner){
case (Some(originRepository), Some(forkedRepository)) => { Some(forkedRepository.name)
using( } else {
Git.open(getRepositoryDir(originOwner, repository.name)), forkedRepository.repository.originRepositoryName.orElse {
Git.open(getRepositoryDir(forkedOwner, repository.name)) getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
){ case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
pulls.html.mergecheck(
checkConflict(originOwner, repository.name, originBranch, forkedOwner, repository.name, forkedBranch))
} }
};
originRepository <- getRepository(originOwner, originRepositoryName, baseUrl)
) yield {
using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
){ case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
pulls.html.mergecheck(
checkConflict(originRepository.owner, originRepository.name, originBranch,
forkedRepository.owner, forkedRepository.name, forkedBranch))
} }
case _ => NotFound() }) getOrElse NotFound
}
}) })
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) => post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
@@ -303,7 +344,7 @@ trait PullRequestsControllerBase extends ControllerBase {
issueId = issueId, issueId = issueId,
originBranch = form.targetBranch, originBranch = form.targetBranch,
requestUserName = form.requestUserName, requestUserName = form.requestUserName,
requestRepositoryName = repository.name, requestRepositoryName = form.requestRepositoryName,
requestBranch = form.requestBranch, requestBranch = form.requestBranch,
commitIdFrom = form.commitIdFrom, commitIdFrom = form.commitIdFrom,
commitIdTo = form.commitIdTo) commitIdTo = form.commitIdTo)
@@ -311,7 +352,7 @@ trait PullRequestsControllerBase extends ControllerBase {
// fetch requested branch // fetch requested branch
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.fetch git.fetch
.setRemote(getRepositoryDir(form.requestUserName, repository.name).toURI.toString) .setRemote(getRepositoryDir(form.requestUserName, form.requestRepositoryName).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head")) .setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head"))
.call .call
} }
@@ -336,12 +377,12 @@ trait PullRequestsControllerBase extends ControllerBase {
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git => using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
val remoteRefName = s"refs/heads/${branch}" val remoteRefName = s"refs/heads/${branch}"
val tmpRefName = s"refs/merge-check/${userName}/${branch}" val tmpRefName = s"refs/merge-check/${userName}/${branch}"
val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true)
withTmpRefSpec(new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true), git) { ref => try {
// fetch objects from origin repository branch // fetch objects from origin repository branch
git.fetch git.fetch
.setRemote(getRepositoryDir(userName, repositoryName).toURI.toString) .setRemote(getRepositoryDir(userName, repositoryName).toURI.toString)
.setRefSpecs(ref) .setRefSpecs(refSpec)
.call .call
// merge conflict check // merge conflict check
@@ -353,6 +394,10 @@ trait PullRequestsControllerBase extends ControllerBase {
} catch { } catch {
case e: NoMergeBaseException => true case e: NoMergeBaseException => true
} }
} finally {
val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
refUpdate.setForceUpdate(true)
refUpdate.delete()
} }
} }
} }
@@ -405,8 +450,7 @@ trait PullRequestsControllerBase extends ControllerBase {
private def getForkedCommitId(oldGit: Git, newGit: Git, userName: String, repositoryName: String, branch: String, private def getForkedCommitId(oldGit: Git, newGit: Git, userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): String = requestUserName: String, requestRepositoryName: String, requestBranch: String): String =
JGitUtil.getCommitLogs(newGit, requestBranch, true){ commit => JGitUtil.getCommitLogs(newGit, requestBranch, true){ commit =>
existsCommitId(userName, repositoryName, commit.getName) && existsCommitId(userName, repositoryName, commit.getName) && JGitUtil.getBranchesOfCommit(oldGit, commit.getName).contains(branch)
JGitUtil.getBranchesOfCommit(oldGit, commit.getName).contains(branch)
}.head.id }.head.id
private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String, private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String,
@@ -421,7 +465,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit => val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
new CommitInfo(revCommit) new CommitInfo(revCommit)
}.toList.splitWith{ (commit1, commit2) => }.toList.splitWith { (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time) view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
} }

View File

@@ -21,12 +21,13 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
with OwnerAuthenticator with UsersAuthenticator => with OwnerAuthenticator with UsersAuthenticator =>
// for repository options // for repository options
case class OptionsForm(description: Option[String], defaultBranch: String, isPrivate: Boolean) case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean)
val optionsForm = mapping( val optionsForm = mapping(
"description" -> trim(label("Description" , optional(text()))), "repositoryName" -> trim(label("Description" , text(required, maxlength(40), identifier, renameRepositoryName))),
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))), "description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())) "defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
"isPrivate" -> trim(label("Repository Type", boolean()))
)(OptionsForm.apply) )(OptionsForm.apply)
// for collaborator addition // for collaborator addition
@@ -43,6 +44,13 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
"url" -> trim(label("url", text(required, webHook))) "url" -> trim(label("url", text(required, webHook)))
)(WebHookForm.apply) )(WebHookForm.apply)
// for transfer ownership
case class TransferOwnerShipForm(newOwner: String)
val transferForm = mapping(
"newOwner" -> trim(label("New owner", text(required, transferUser)))
)(TransferOwnerShipForm.apply)
/** /**
* Redirect to the Options page. * Redirect to the Options page.
*/ */
@@ -70,8 +78,21 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
repository.repository.isPrivate repository.repository.isPrivate
} getOrElse form.isPrivate } getOrElse form.isPrivate
) )
// Change repository name
if(repository.name != form.repositoryName){
// Update database
renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
// Move git repository
defining(getRepositoryDir(repository.owner, repository.name)){ dir =>
FileUtils.moveDirectory(dir, getRepositoryDir(repository.owner, form.repositoryName))
}
// Move wiki repository
defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName))
}
}
flash += "info" -> "Repository settings has been updated." flash += "info" -> "Repository settings has been updated."
redirect(s"/${repository.owner}/${repository.name}/settings/options") redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
}) })
/** /**
@@ -138,17 +159,13 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
.setMaxCount(3) .setMaxCount(3)
.call.iterator.asScala.map(new CommitInfo(_)) .call.iterator.asScala.map(new CommitInfo(_))
val webHookURLs = getWebHookURLs(repository.owner, repository.name) getWebHookURLs(repository.owner, repository.name) match {
if(webHookURLs.nonEmpty){ case webHookURLs if(webHookURLs.nonEmpty) =>
val owner = getAccountByUserName(repository.owner).get for(ownerAccount <- getAccountByUserName(repository.owner)){
callWebHook(repository.owner, repository.name, webHookURLs, callWebHook(repository.owner, repository.name, webHookURLs,
WebHookPayload( WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount))
git, }
owner, case _ =>
"refs/heads/" + repository.repository.defaultBranch,
repository,
commits.toList,
owner))
} }
flash += "info" -> "Test payload deployed!" flash += "info" -> "Test payload deployed!"
@@ -157,10 +174,30 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
}) })
/** /**
* Display the delete repository page. * Display the danger zone.
*/ */
get("/:owner/:repository/settings/delete")(ownerOnly { get("/:owner/:repository/settings/danger")(ownerOnly {
settings.html.delete(_) settings.html.danger(_)
})
/**
* Transfer repository ownership.
*/
post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) =>
// Change repository owner
if(repository.owner != form.newOwner){
// Update database
renameRepository(repository.owner, repository.name, form.newOwner, repository.name)
// Move git repository
defining(getRepositoryDir(repository.owner, repository.name)){ dir =>
FileUtils.moveDirectory(dir, getRepositoryDir(form.newOwner, repository.name))
}
// Move wiki repository
defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name))
}
}
redirect(s"/${form.newOwner}/${repository.name}")
}) })
/** /**
@@ -199,4 +236,32 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
} }
} }
/**
* Duplicate check for the rename repository name.
*/
private def renameRepositoryName: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
params.get("repository").filter(_ != value).flatMap { _ =>
params.get("owner").flatMap { userName =>
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
}
}
}
/**
* Provides Constraint to validate the repository transfer user.
*/
private def transferUser: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
getAccountByUserName(value) match {
case None => Some("User does not exist.")
case Some(x) => if(x.userName == params("owner")){
Some("This is current repository owner.")
} else {
params.get("repository").flatMap { repositoryName =>
getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map{ _ => "User already has same repository." }
}
}
}
}
} }

View File

@@ -3,7 +3,7 @@ package app
import util.Directory._ import util.Directory._
import util.Implicits._ import util.Implicits._
import util.ControlUtil._ import util.ControlUtil._
import _root_.util.{ReferrerAuthenticator, JGitUtil, FileUtil, StringUtil} import _root_.util._
import service._ import service._
import org.scalatra._ import org.scalatra._
import java.io.File import java.io.File
@@ -12,15 +12,16 @@ import org.eclipse.jgit.lib._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.eclipse.jgit.treewalk._ import org.eclipse.jgit.treewalk._
import java.util.zip.{ZipEntry, ZipOutputStream} import java.util.zip.{ZipEntry, ZipOutputStream}
import scala.Some
class RepositoryViewerController extends RepositoryViewerControllerBase class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ReferrerAuthenticator with RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator
/** /**
* The repository viewer. * The repository viewer.
*/ */
trait RepositoryViewerControllerBase extends ControllerBase { trait RepositoryViewerControllerBase extends ControllerBase {
self: RepositoryService with AccountService with ReferrerAuthenticator => self: RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
/** /**
* Returns converted HTML from Markdown for preview. * Returns converted HTML from Markdown for preview.
@@ -150,10 +151,25 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val revCommit = git.log.add(git.getRepository.resolve(branchName)).setMaxCount(1).call.iterator.next val revCommit = git.log.add(git.getRepository.resolve(branchName)).setMaxCount(1).call.iterator.next
(branchName, revCommit.getCommitterIdent.getWhen) (branchName, revCommit.getCommitterIdent.getWhen)
} }
repo.html.branches(branchInfo, repository) repo.html.branches(branchInfo, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
} }
}) })
/**
* Deletes branch.
*/
get("/:owner/:repository/delete/:branchName")(collaboratorsOnly { repository =>
val branchName = params("branchName")
val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.branchDelete().setBranchNames(branchName).call()
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
}
}
redirect(s"/${repository.owner}/${repository.name}/branches")
})
/** /**
* Displays tags. * Displays tags.
*/ */
@@ -175,7 +191,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} }
workDir.mkdirs workDir.mkdirs
val zipFile = new File(workDir, (if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip") val zipFile = new File(workDir, repository.name + "-" +
(if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip")
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision)) val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
@@ -204,6 +221,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} }
contentType = "application/octet-stream" contentType = "application/octet-stream"
response.setHeader("Content-Disposition", s"attachment; filename=${zipFile.getName}")
zipFile zipFile
} else { } else {
BadRequest BadRequest

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

@@ -1,58 +0,0 @@
package app
import service._
import jp.sf.amateras.scalatra.forms._
import util.Implicits._
import util.StringUtil._
import util.Keys
class SignInController extends SignInControllerBase with SystemSettingsService with AccountService
trait SignInControllerBase extends ControllerBase { self: SystemSettingsService with AccountService =>
case class SignInForm(userName: String, password: String)
val form = mapping(
"userName" -> trim(label("Username", text(required))),
"password" -> trim(label("Password", text(required)))
)(SignInForm.apply)
get("/signin"){
val redirect = params.get("redirect")
if(redirect.isDefined && redirect.get.startsWith("/")){
session.setAttribute(Keys.Session.Redirect, redirect.get)
}
html.signin(loadSystemSettings())
}
post("/signin", form){ form =>
authenticate(loadSystemSettings(), form.userName, form.password) match {
case Some(account) => signin(account)
case None => redirect("/signin")
}
}
get("/signout"){
session.invalidate
redirect("/")
}
/**
* Set account information into HttpSession and redirect.
*/
private def signin(account: model.Account) = {
session.setAttribute(Keys.Session.LoginAccount, account)
updateLastLoginDate(account.userName)
session.getAndRemove[String](Keys.Session.Redirect).map { redirectUrl =>
if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){
redirect("/")
} else {
redirect(redirectUrl)
}
}.getOrElse {
redirect("/")
}
}
}

View File

@@ -13,6 +13,7 @@ trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
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())),
@@ -33,6 +34,7 @@ trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
"bindPassword" -> trim(label("Bind Password", optional(text()))), "bindPassword" -> trim(label("Bind Password", optional(text()))),
"baseDN" -> trim(label("Base DN", text(required))), "baseDN" -> trim(label("Base DN", text(required))),
"userNameAttribute" -> trim(label("User name attribute", text(required))), "userNameAttribute" -> trim(label("User name attribute", text(required))),
"fullNameAttribute" -> trim(label("Full name attribute", optional(text()))),
"mailAttribute" -> trim(label("Mail address attribute", text(required))), "mailAttribute" -> trim(label("Mail address attribute", text(required))),
"tls" -> trim(label("Enable TLS", optional(boolean()))), "tls" -> trim(label("Enable TLS", optional(boolean()))),
"keystore" -> trim(label("Keystore", optional(text()))) "keystore" -> trim(label("Keystore", optional(text())))

View File

@@ -188,16 +188,16 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
private def conflictForNew: Constraint = new Constraint(){ private def conflictForNew: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = { override def validate(name: String, value: String, messages: Messages): Option[String] = {
optionIf(targetWikiPage.nonEmpty){ targetWikiPage.map { _ =>
Some("Someone has created the wiki since you started. Please reload this page and re-apply your changes.") "Someone has created the wiki since you started. Please reload this page and re-apply your changes."
} }
} }
} }
private def conflictForEdit: Constraint = new Constraint(){ private def conflictForEdit: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = { override def validate(name: String, value: String, messages: Messages): Option[String] = {
optionIf(targetWikiPage.map(_.id != params("id")).getOrElse(false)){ targetWikiPage.filter(_.id != params("id")).map{ _ =>
Some("Someone has edited the wiki since you started. Please reload this page and re-apply your changes.") "Someone has edited the wiki since you started. Please reload this page and re-apply your changes."
} }
} }
} }

View File

@@ -36,11 +36,15 @@ trait AccountService {
*/ */
private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) = { private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) = {
LDAPUtil.authenticate(settings.ldap.get, userName, password) match { LDAPUtil.authenticate(settings.ldap.get, userName, password) match {
case Right(mailAddress) => { case Right(ldapUserInfo) => {
// Create or update account by LDAP information // Create or update account by LDAP information
getAccountByUserName(userName) match { getAccountByUserName(userName, true) match {
case Some(x) => updateAccount(x.copy(mailAddress = mailAddress)) case Some(x) if(!x.isRemoved) => updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName))
case None => createAccount(userName, "", userName, mailAddress, false, None) case Some(x) if(x.isRemoved) => {
logger.info(s"LDAP Authentication Failed: Account is already registered but disabled..")
defaultAuthentication(userName, password)
}
case None => createAccount(userName, "", ldapUserInfo.fullName, ldapUserInfo.mailAddress, false, None)
} }
getAccountByUserName(userName) getAccountByUserName(userName)
} }

View File

@@ -343,7 +343,7 @@ object IssuesService {
def toURL: String = def toURL: String =
"?" + List( "?" + List(
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(" "))), if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
milestoneId.map { id => "milestone=" + (id match { milestoneId.map { id => "milestone=" + (id match {
case Some(x) => x.toString case Some(x) => x.toString
case None => "none" case None => "none"
@@ -364,7 +364,7 @@ object IssuesService {
def apply(request: HttpServletRequest): IssueSearchCondition = def apply(request: HttpServletRequest): IssueSearchCondition =
IssueSearchCondition( IssueSearchCondition(
param(request, "labels").map(_.split(" ").toSet).getOrElse(Set.empty), param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
param(request, "milestone").map{ param(request, "milestone").map{
case "none" => None case "none" => None
case x => x.toIntOpt case x => x.toIntOpt

View File

@@ -39,6 +39,67 @@ trait RepositoryService { self: AccountService =>
IssueId insert (userName, repositoryName, 0) IssueId insert (userName, repositoryName, 0)
} }
def renameRepository(oldUserName: String, oldRepositoryName: String, newUserName: String, newRepositoryName: String): Unit = {
(Query(Repositories) filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository =>
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
val webHooks = Query(WebHooks ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val milestones = Query(Milestones ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueId = Query(IssueId ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issues = Query(Issues ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val pullRequests = Query(PullRequests ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val labels = Query(Labels ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueComments = Query(IssueComments).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueLabels = Query(IssueLabels ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val collaborators = Query(Collaborators).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val commitLog = Query(CommitLog ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
val activities = Query(Activities ).filter(_.byRepository(oldUserName, oldRepositoryName)).list
Repositories.filter { t =>
(t.originUserName is oldUserName.bind) && (t.originRepositoryName is oldRepositoryName.bind)
}.map { t => t.originUserName ~ t.originRepositoryName }.update(newUserName, newRepositoryName)
Repositories.filter { t =>
(t.parentUserName is oldUserName.bind) && (t.parentRepositoryName is oldRepositoryName.bind)
}.map { t => t.originUserName ~ t.originRepositoryName }.update(newUserName, newRepositoryName)
PullRequests.filter { t =>
t.requestRepositoryName is oldRepositoryName.bind
}.map { t => t.requestUserName ~ t.requestRepositoryName }.update(newUserName, newRepositoryName)
deleteRepository(oldUserName, oldRepositoryName)
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
Issues .insertAll(issues .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueLabels .insertAll(issueLabels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Collaborators .insertAll(collaborators .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
CommitLog .insertAll(commitLog .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
Activities .insertAll(activities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
// Update activity messages
val updateActivities = Activities.filter { t =>
(t.message like s"%:${oldUserName}/${oldRepositoryName}]%") ||
(t.message like s"%:${oldUserName}/${oldRepositoryName}#%")
}.map { t => t.activityId ~ t.message }.list
updateActivities.foreach { case (activityId, message) =>
Activities.filter(_.activityId is activityId.bind).map(_.message).update(
message
.replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]")
.replace(s"[branch:${oldUserName}/${oldRepositoryName}#" ,s"[branch:${newUserName}/${newRepositoryName}#")
.replace(s"[tag:${oldUserName}/${oldRepositoryName}#" ,s"[tag:${newUserName}/${newRepositoryName}#")
.replace(s"[pullreq:${oldUserName}/${oldRepositoryName}#",s"[pullreq:${newUserName}/${newRepositoryName}#")
.replace(s"[issue:${oldUserName}/${oldRepositoryName}#" ,s"[issue:${newUserName}/${newRepositoryName}#")
)
}
}
}
def deleteRepository(userName: String, repositoryName: String): Unit = { def deleteRepository(userName: String, repositoryName: String): Unit = {
Activities .filter(_.byRepository(userName, repositoryName)).delete Activities .filter(_.byRepository(userName, repositoryName)).delete
CommitLog .filter(_.byRepository(userName, repositoryName)).delete CommitLog .filter(_.byRepository(userName, repositoryName)).delete
@@ -207,11 +268,11 @@ trait RepositoryService { self: AccountService =>
}.length).first }.length).first
def getForkedRepositories(userName: String, repositoryName: String): List[String] = def getForkedRepositories(userName: String, repositoryName: String): List[(String, String)] =
Query(Repositories).filter { t => Query(Repositories).filter { t =>
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind) (t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
} }
.sortBy(_.userName asc).map(_.userName).list .sortBy(_.userName asc).map(t => t.userName ~ t.repositoryName).list
} }

View File

@@ -8,6 +8,7 @@ trait SystemSettingsService {
def saveSystemSettings(settings: SystemSettings): Unit = { def saveSystemSettings(settings: SystemSettings): Unit = {
defining(new java.util.Properties()){ props => defining(new java.util.Properties()){ props =>
settings.baseUrl.foreach(props.setProperty(BaseURL, _))
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString) props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
props.setProperty(Gravatar, settings.gravatar.toString) props.setProperty(Gravatar, settings.gravatar.toString)
props.setProperty(Notification, settings.notification.toString) props.setProperty(Notification, settings.notification.toString)
@@ -31,6 +32,7 @@ trait SystemSettingsService {
ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x)) ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
props.setProperty(LdapBaseDN, ldap.baseDN) props.setProperty(LdapBaseDN, ldap.baseDN)
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute) props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute) props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute)
ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString)) ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x)) ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
@@ -47,6 +49,7 @@ trait SystemSettingsService {
props.load(new java.io.FileInputStream(GitBucketConf)) props.load(new java.io.FileInputStream(GitBucketConf))
} }
SystemSettings( SystemSettings(
getOptionValue(props, BaseURL, None),
getValue(props, AllowAccountRegistration, false), getValue(props, AllowAccountRegistration, false),
getValue(props, Gravatar, true), getValue(props, Gravatar, true),
getValue(props, Notification, false), getValue(props, Notification, false),
@@ -71,6 +74,7 @@ trait SystemSettingsService {
getOptionValue(props, LdapBindPassword, None), getOptionValue(props, LdapBindPassword, None),
getValue(props, LdapBaseDN, ""), getValue(props, LdapBaseDN, ""),
getValue(props, LdapUserNameAttribute, ""), getValue(props, LdapUserNameAttribute, ""),
getOptionValue(props, LdapFullNameAttribute, None),
getValue(props, LdapMailAddressAttribute, ""), getValue(props, LdapMailAddressAttribute, ""),
getOptionValue[Boolean](props, LdapTls, None), getOptionValue[Boolean](props, LdapTls, None),
getOptionValue(props, LdapKeystore, None))) getOptionValue(props, LdapKeystore, None)))
@@ -87,6 +91,7 @@ object SystemSettingsService {
import scala.reflect.ClassTag import scala.reflect.ClassTag
case class SystemSettings( case class SystemSettings(
baseUrl: Option[String],
allowAccountRegistration: Boolean, allowAccountRegistration: Boolean,
gravatar: Boolean, gravatar: Boolean,
notification: Boolean, notification: Boolean,
@@ -101,6 +106,7 @@ object SystemSettingsService {
bindPassword: Option[String], bindPassword: Option[String],
baseDN: String, baseDN: String,
userNameAttribute: String, userNameAttribute: String,
fullNameAttribute: Option[String],
mailAttribute: String, mailAttribute: String,
tls: Option[Boolean], tls: Option[Boolean],
keystore: Option[String]) keystore: Option[String])
@@ -117,6 +123,7 @@ object SystemSettingsService {
val DefaultSmtpPort = 25 val DefaultSmtpPort = 25
val DefaultLdapPort = 389 val DefaultLdapPort = 389
private val BaseURL = "base_url"
private val AllowAccountRegistration = "allow_account_registration" private val AllowAccountRegistration = "allow_account_registration"
private val Gravatar = "gravatar" private val Gravatar = "gravatar"
private val Notification = "notification" private val Notification = "notification"
@@ -134,6 +141,7 @@ object SystemSettingsService {
private val LdapBindPassword = "ldap.bind_password" private val LdapBindPassword = "ldap.bind_password"
private val LdapBaseDN = "ldap.baseDN" private val LdapBaseDN = "ldap.baseDN"
private val LdapUserNameAttribute = "ldap.username_attribute" private val LdapUserNameAttribute = "ldap.username_attribute"
private val LdapFullNameAttribute = "ldap.fullname_attribute"
private val LdapMailAddressAttribute = "ldap.mail_attribute" private val LdapMailAddressAttribute = "ldap.mail_attribute"
private val LdapTls = "ldap.tls" private val LdapTls = "ldap.tls"
private val LdapKeystore = "ldap.keystore" private val LdapKeystore = "ldap.keystore"

View File

@@ -3,7 +3,7 @@ package service
import java.util.Date import java.util.Date
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import util.{PatchUtil, Directory, JGitUtil, LockUtil} import util._
import _root_.util.ControlUtil._ import _root_.util.ControlUtil._
import org.eclipse.jgit.treewalk.{TreeWalk, CanonicalTreeParser} import org.eclipse.jgit.treewalk.{TreeWalk, CanonicalTreeParser}
import org.eclipse.jgit.lib._ import org.eclipse.jgit.lib._
@@ -14,6 +14,7 @@ import java.io.ByteArrayInputStream
import org.eclipse.jgit.patch._ import org.eclipse.jgit.patch._
import org.eclipse.jgit.api.errors.PatchFormatException import org.eclipse.jgit.api.errors.PatchFormatException
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
import scala.Some
object WikiService { object WikiService {
@@ -59,11 +60,12 @@ trait WikiService {
*/ */
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = { def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
optionIf(!JGitUtil.isEmpty(git)){ if(!JGitUtil.isEmpty(git)){
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file => JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
WikiPageInfo(file.name, new String(git.getRepository.open(file.id).getBytes, "UTF-8"), file.committer, file.time, file.commitId) WikiPageInfo(file.name, StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes),
file.committer, file.time, file.commitId)
} }
} } else None
} }
} }
@@ -72,7 +74,7 @@ trait WikiService {
*/ */
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] = def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] =
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
optionIf(!JGitUtil.isEmpty(git)){ if(!JGitUtil.isEmpty(git)){
val index = path.lastIndexOf('/') val index = path.lastIndexOf('/')
val parentPath = if(index < 0) "." else path.substring(0, index) val parentPath = if(index < 0) "." else path.substring(0, index)
val fileName = if(index < 0) path else path.substring(index + 1) val fileName = if(index < 0) path else path.substring(index + 1)
@@ -80,7 +82,7 @@ trait WikiService {
JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file => JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file =>
git.getRepository.open(file.id).getBytes git.getRepository.open(file.id).getBytes
} }
} } else None
} }
/** /**
@@ -239,7 +241,7 @@ trait WikiService {
} }
} }
optionIf(created || updated || removed){ if(created || updated || removed){
builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")))) builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish() builder.finish()
val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress, val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress,
@@ -256,7 +258,7 @@ trait WikiService {
}) })
Some(newHeadId) Some(newHeadId)
} } else None
} }
} }
} }

View File

@@ -50,6 +50,9 @@ 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, 9),
Version(1, 8), Version(1, 8),
Version(1, 7), Version(1, 7),
Version(1, 6), Version(1, 6),
@@ -120,7 +123,7 @@ class AutoUpdateListener extends ServletContextListener {
System.setProperty("gitbucket.home", datadir) System.setProperty("gitbucket.home", datadir)
} }
org.h2.Driver.load() org.h2.Driver.load()
event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome}") event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true")
logger.debug("Start schema update") logger.debug("Start schema update")
defining(getConnection(event.getServletContext)){ conn => defining(getConnection(event.getServletContext)){ conn =>

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._
@@ -56,10 +56,10 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
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 userName = request.getAttribute(Keys.Request.UserName).asInstanceOf[String] val pusher = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
logger.debug("requestURI: " + request.getRequestURI) logger.debug("requestURI: " + request.getRequestURI)
logger.debug("userName:" + userName) logger.debug("pusher:" + pusher)
defining(request.paths){ paths => defining(request.paths){ paths =>
val owner = paths(1) val owner = paths(1)
@@ -69,7 +69,9 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
logger.debug("repository:" + owner + "/" + repository) logger.debug("repository:" + owner + "/" + repository)
logger.debug("baseURL:" + baseURL) logger.debug("baseURL:" + baseURL)
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, userName, baseURL)) if(!repository.endsWith(".wiki")){
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseURL))
}
receivePack receivePack
} }
} }
@@ -77,101 +79,107 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String, userName: 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])
def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = { def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git => try {
commands.asScala.foreach { command => using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}") commands.asScala.foreach { command =>
val commits = command.getType match { logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
case ReceiveCommand.Type.DELETE => Nil val commits = command.getType match {
case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name) case ReceiveCommand.Type.DELETE => Nil
} case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
val refName = command.getRefName.split("/") }
val branchName = refName.drop(2).mkString("/") val refName = command.getRefName.split("/")
val branchName = refName.drop(2).mkString("/")
// Extract new commit and apply issue comment // Extract new commit and apply issue comment
val newCommits = if(commits.size > 1000){ val newCommits = if(commits.size > 1000){
val existIds = getAllCommitIds(owner, repository) val existIds = getAllCommitIds(owner, repository)
commits.flatMap { commit => commits.flatMap { commit =>
optionIf(!existIds.contains(commit.id)){ if(!existIds.contains(commit.id)){
createIssueComment(commit) createIssueComment(commit)
Some(commit) Some(commit)
} else None
}
} else {
commits.flatMap { commit =>
if(!existsCommitId(owner, repository, commit.id)){
createIssueComment(commit)
Some(commit)
} else None
} }
} }
} else {
commits.flatMap { commit => // batch insert all new commit id
optionIf(!existsCommitId(owner, repository, commit.id)){ insertAllCommitIds(owner, repository, newCommits.map(_.id))
createIssueComment(commit)
Some(commit) // record activity
if(refName(1) == "heads"){
command.getType match {
case ReceiveCommand.Type.CREATE => recordCreateBranchActivity(owner, repository, pusher, branchName)
case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, pusher, branchName, newCommits)
case ReceiveCommand.Type.DELETE => recordDeleteBranchActivity(owner, repository, pusher, branchName)
case _ =>
}
} else if(refName(1) == "tags"){
command.getType match {
case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, pusher, branchName, newCommits)
case ReceiveCommand.Type.DELETE => recordDeleteTagActivity(owner, repository, pusher, branchName, newCommits)
case _ =>
} }
} }
}
// batch insert all new commit id if(refName(1) == "heads"){
insertAllCommitIds(owner, repository, newCommits.map(_.id)) command.getType match {
case ReceiveCommand.Type.CREATE |
ReceiveCommand.Type.UPDATE |
ReceiveCommand.Type.UPDATE_NONFASTFORWARD =>
updatePullRequests(branchName)
case _ =>
}
}
// record activity // close issues
if(refName(1) == "heads"){ val defaultBranch = getRepository(owner, repository, baseURL).get.repository.defaultBranch
command.getType match { if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){
case ReceiveCommand.Type.CREATE => recordCreateBranchActivity(owner, repository, userName, branchName) git.log.addRange(command.getOldId, command.getNewId).call.asScala.foreach { commit =>
case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, userName, branchName, newCommits) closeIssuesFromMessage(commit.getFullMessage, pusher, owner, repository)
case ReceiveCommand.Type.DELETE => recordDeleteBranchActivity(owner, repository, userName, branchName) }
}
// call web hook
getWebHookURLs(owner, repository) match {
case webHookURLs if(webHookURLs.nonEmpty) =>
for(pusherAccount <- getAccountByUserName(pusher);
ownerAccount <- getAccountByUserName(owner);
repositoryInfo <- getRepository(owner, repository, baseURL)){
callWebHook(owner, repository, webHookURLs,
WebHookPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount))
}
case _ => case _ =>
} }
} else if(refName(1) == "tags"){
command.getType match {
case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, userName, branchName, newCommits)
case ReceiveCommand.Type.DELETE => recordDeleteTagActivity(owner, repository, userName, branchName, newCommits)
case _ =>
}
}
if(refName(1) == "heads"){
command.getType match {
case ReceiveCommand.Type.CREATE |
ReceiveCommand.Type.UPDATE |
ReceiveCommand.Type.UPDATE_NONFASTFORWARD =>
updatePullRequests(branchName)
case _ =>
}
}
// 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, userName, owner, repository)
}
}
// call web hook
val webHookURLs = getWebHookURLs(owner, repository)
if(webHookURLs.nonEmpty){
val payload = WebHookPayload(
git,
getAccountByUserName(userName).get,
command.getRefName,
getRepository(owner, repository, baseURL).get,
newCommits,
getAccountByUserName(owner).get)
callWebHook(owner, repository, webHookURLs, payload)
} }
} }
// update repository last modified time.
updateLastActivityDate(owner, repository)
} catch {
case ex: Exception => {
logger.error(ex.toString, ex)
throw ex
}
} }
// update repository last modified time.
updateLastActivityDate(owner, repository)
} }
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(getAccountByUserName(commit.committer).isDefined && getIssue(owner, repository, issueId).isDefined){ getAccountByMailAddress(commit.mailAddress).foreach { account =>
createComment(owner, repository, commit.committer, issueId.toInt, commit.fullMessage + " " + commit.id, "commit") createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
}
} }
} }
} }

View File

@@ -4,6 +4,7 @@ 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 org.eclipse.jgit.transport.RefSpec
import scala.language.reflectiveCalls
/** /**
* Provides control facilities. * Provides control facilities.
@@ -24,12 +25,12 @@ object ControlUtil {
} }
def using[T](git: Git)(f: Git => T): T = def using[T](git: Git)(f: Git => T): T =
try f(git) finally git.getRepository.close try f(git) finally git.getRepository.close()
def using[T](git1: Git, git2: Git)(f: (Git, Git) => T): T = def using[T](git1: Git, git2: Git)(f: (Git, Git) => T): T =
try f(git1, git2) finally { try f(git1, git2) finally {
git1.getRepository.close git1.getRepository.close()
git2.getRepository.close git2.getRepository.close()
} }
def using[T](revWalk: RevWalk)(f: RevWalk => T): T = def using[T](revWalk: RevWalk)(f: RevWalk => T): T =
@@ -39,22 +40,14 @@ object ControlUtil {
try f(treeWalk) finally treeWalk.release() try f(treeWalk) finally treeWalk.release()
def withTmpRefSpec[T](ref: RefSpec, git: Git)(f: RefSpec => T): T = { // def withTmpRefSpec[T](ref: RefSpec, git: Git)(f: RefSpec => T): T = {
try { // try {
f(ref) // f(ref)
} finally { // } finally {
val refUpdate = git.getRepository.updateRef(ref.getDestination) // val refUpdate = git.getRepository.updateRef(ref.getDestination)
refUpdate.setForceUpdate(true) // refUpdate.setForceUpdate(true)
refUpdate.delete() // refUpdate.delete()
} // }
} // }
def executeIf(condition: => Boolean)(action: => Unit): Boolean =
if(condition){
action
true
} else false
def optionIf[T](condition: => Boolean)(action: => Option[T]): Option[T] =
if(condition) action else None
} }

View File

@@ -2,6 +2,7 @@ package util
import java.io.File import java.io.File
import util.ControlUtil._ import util.ControlUtil._
import org.apache.commons.io.FileUtils
/** /**
* Provides directories used by GitBucket. * Provides directories used by GitBucket.
@@ -14,8 +15,16 @@ object Directory {
case _ => scala.util.Properties.envOrNone("GITBUCKET_HOME") match { case _ => scala.util.Properties.envOrNone("GITBUCKET_HOME") match {
// environment variable GITBUCKET_HOME // environment variable GITBUCKET_HOME
case Some(env) => new File(env) case Some(env) => new File(env)
// default is HOME/gitbucket // default is HOME/.gitbucket
case None => new File(System.getProperty("user.home"), "gitbucket") case None => {
val oldHome = new File(System.getProperty("user.home"), "gitbucket")
if(oldHome.exists && oldHome.isDirectory && new File(oldHome, "version").exists){
//FileUtils.moveDirectory(oldHome, newHome)
oldHome
} else {
new File(System.getProperty("user.home"), ".gitbucket")
}
}
} }
}).getAbsolutePath }).getAbsolutePath

View File

@@ -63,9 +63,9 @@ object FileUtil {
if(dir.exists()){ if(dir.exists()){
FileUtils.deleteDirectory(dir) FileUtils.deleteDirectory(dir)
} }
try{ try {
action(dir) action(dir)
}finally{ } finally {
FileUtils.deleteDirectory(dir) FileUtils.deleteDirectory(dir)
} }
} }

View File

@@ -79,9 +79,9 @@ object JGitUtil {
} }
val description = defining(fullMessage.trim.indexOf("\n")){ i => val description = defining(fullMessage.trim.indexOf("\n")){ i =>
optionIf(i >= 0){ if(i >= 0){
Some(fullMessage.trim.substring(i).trim) Some(fullMessage.trim.substring(i).trim)
} } else None
} }
} }

View File

@@ -18,51 +18,49 @@ object LDAPUtil {
/** /**
* Try authentication by LDAP using given configuration. * Try authentication by LDAP using given configuration.
* Returns Right(mailAddress) if authentication is successful, otherwise Left(errorMessage). * Returns Right(LDAPUserInfo) if authentication is successful, otherwise Left(errorMessage).
*/ */
def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, String] = { def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, LDAPUserInfo] = {
bind( bind(
ldapSettings.host, host = ldapSettings.host,
ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort), port = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
ldapSettings.bindDN.getOrElse(""), dn = ldapSettings.bindDN.getOrElse(""),
ldapSettings.bindPassword.getOrElse(""), password = ldapSettings.bindPassword.getOrElse(""),
ldapSettings.tls.getOrElse(false), tls = ldapSettings.tls.getOrElse(false),
ldapSettings.keystore.getOrElse("") keystore = ldapSettings.keystore.getOrElse(""),
) match { error = "System LDAP authentication failed."
case Some(conn) => { ){ conn =>
withConnection(conn) { conn => findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match {
findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match { case Some(userDN) => userAuthentication(ldapSettings, userDN, userName, password)
case Some(userDN) => userAuthentication(ldapSettings, userDN, password) case None => Left("User does not exist.")
case None => Left("User does not exist.")
}
}
} }
case None => Left("System LDAP authentication failed.")
} }
} }
private def userAuthentication(ldapSettings: Ldap, userDN: String, password: String): Either[String, String] = { private def userAuthentication(ldapSettings: Ldap, userDN: String, userName: String, password: String): Either[String, LDAPUserInfo] = {
bind( bind(
ldapSettings.host, host = ldapSettings.host,
ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort), port = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
userDN, dn = userDN,
password, password = password,
ldapSettings.tls.getOrElse(false), tls = ldapSettings.tls.getOrElse(false),
ldapSettings.keystore.getOrElse("") keystore = ldapSettings.keystore.getOrElse(""),
) match { error = "User LDAP Authentication Failed."
case Some(conn) => { ){ conn =>
withConnection(conn) { conn => findMailAddress(conn, userDN, ldapSettings.mailAttribute) match {
findMailAddress(conn, userDN, ldapSettings.mailAttribute) match { case Some(mailAddress) => Right(LDAPUserInfo(
case Some(mailAddress) => Right(mailAddress) userName = userName,
case None => Left("Can't find mail address.") fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute =>
} findFullName(conn, userDN, fullNameAttribute)
} }.getOrElse(userName),
mailAddress = mailAddress))
case None => Left("Can't find mail address.")
} }
case None => Left("User LDAP Authentication Failed.")
} }
} }
private def bind(host: String, port: Int, dn: String, password: String, tls: Boolean, keystore: String): Option[LDAPConnection] = { private def bind[A](host: String, port: Int, dn: String, password: String, tls: Boolean, keystore: String, error: String)
(f: LDAPConnection => Either[String, A]): Either[String, A] = {
if (tls) { if (tls) {
// Dynamically set Sun as the security provider // Dynamically set Sun as the security provider
Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider()) Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider())
@@ -87,7 +85,9 @@ object LDAPUtil {
// Bind to the server // Bind to the server
conn.bind(LDAP_VERSION, dn, password.getBytes) conn.bind(LDAP_VERSION, dn, password.getBytes)
Some(conn) // Execute a given function and returns a its result
f(conn)
} catch { } catch {
case e: Exception => { case e: Exception => {
// Provide more information if something goes wrong // Provide more information if something goes wrong
@@ -96,20 +96,15 @@ object LDAPUtil {
if (conn.isConnected) { if (conn.isConnected) {
conn.disconnect() conn.disconnect()
} }
// Returns an error message
None Left(error)
} }
} }
} }
private def withConnection[T](conn: LDAPConnection)(f: LDAPConnection => T): T = { /**
try { * Search a specified user and returns userDN if exists.
f(conn) */
} finally {
conn.disconnect()
}
}
private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String): Option[String] = { private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String): Option[String] = {
@tailrec @tailrec
def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = { def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = {
@@ -130,8 +125,18 @@ object LDAPUtil {
private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] = private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] =
defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false)){ results => defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false)){ results =>
optionIf (results.hasMore) { if(results.hasMore) {
Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue) Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
} } else None
} }
private def findFullName(conn: LDAPConnection, userDN: String, nameAttribute: String): Option[String] =
defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](nameAttribute), false)){ results =>
if(results.hasMore) {
Option(results.next.getAttribute(nameAttribute)).map(_.getStringValue)
} else None
}
case class LDAPUserInfo(userName: String, fullName: String, mailAddress: String)
} }

View File

@@ -3,6 +3,8 @@ package util
import java.net.{URLDecoder, URLEncoder} import java.net.{URLDecoder, URLEncoder}
import org.mozilla.universalchardet.UniversalDetector import org.mozilla.universalchardet.UniversalDetector
import util.ControlUtil._ import util.ControlUtil._
import org.apache.commons.io.input.BOMInputStream
import org.apache.commons.io.IOUtils
object StringUtil { object StringUtil {
@@ -27,7 +29,12 @@ object StringUtil {
def escapeHtml(value: String): String = def escapeHtml(value: String): String =
value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;") value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
def convertFromByteArray(content: Array[Byte]): String = new String(content, detectEncoding(content)) /**
* 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..
*/
def convertFromByteArray(content: Array[Byte]): String =
IOUtils.toString(new BOMInputStream(new java.io.ByteArrayInputStream(content)), detectEncoding(content))
def detectEncoding(content: Array[Byte]): String = def detectEncoding(content: Array[Byte]): String =
defining(new UniversalDetector(null)){ detector => defining(new UniversalDetector(null)){ detector =>
@@ -38,4 +45,14 @@ 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 { matchData => matchData.group(2) }
} }

View File

@@ -7,6 +7,8 @@ import org.parboiled.common.StringUtils
import org.pegdown._ import org.pegdown._
import org.pegdown.ast._ import org.pegdown.ast._
import org.pegdown.LinkRenderer.Rendering import org.pegdown.LinkRenderer.Rendering
import java.text.Normalizer
import java.util.Locale
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
import service.{RequestCache, WikiService} import service.{RequestCache, WikiService}
@@ -110,6 +112,21 @@ class GitBucketHtmlSerializer(
printer.print(' ').print(name).print('=').print('"').print(value).print('"') printer.print(' ').print(name).print('=').print('"').print(value).print('"')
} }
private def printHeaderTag(node: HeaderNode): Unit = {
val tag = s"h${node.getLevel}"
val headerTextString = printChildrenToString(node)
val anchorName = GitBucketHtmlSerializer.generateAnchorName(headerTextString)
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>")
}
override def visit(node: HeaderNode): Unit = {
printHeaderTag(node)
}
override def visit(node: TextNode): Unit = { override def visit(node: TextNode): Unit = {
// convert commit id and username to link. // convert commit id and username to link.
val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText
@@ -120,5 +137,16 @@ class GitBucketHtmlSerializer(
printWithAbbreviations(text) printWithAbbreviations(text)
} }
} }
}
object GitBucketHtmlSerializer {
private val Whitespace = "[\\s]".r
def generateAnchorName(text: String): String = {
val noWhitespace = Whitespace.replaceAllIn(text, "-")
val normalized = Normalizer.normalize(noWhitespace, Normalizer.Form.NFD)
val noSpecialChars = StringUtil.urlEncode(normalized)
noSpecialChars.toLowerCase(Locale.ENGLISH)
}
} }

View File

@@ -1,5 +1,6 @@
@(settings: service.SystemSettingsService.SystemSettings, info: Option[Any])(implicit context: app.Context) @(settings: service.SystemSettingsService.SystemSettings, info: Option[Any])(implicit context: app.Context)
@import context._ @import context._
@import util.Directory._
@import view.helpers._ @import view.helpers._
@html.main("System Settings"){ @html.main("System Settings"){
@menu("system"){ @menu("system"){
@@ -8,9 +9,30 @@
<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 -->
<!--====================================================================-->
<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 --> <!-- Account registration -->
<!--====================================================================--> <!--====================================================================-->
<hr>
<label class="strong">Account registration</label> <label class="strong">Account registration</label>
<fieldset> <fieldset>
<label class="radio"> <label class="radio">
@@ -87,6 +109,13 @@
<span id="error-ldap_userNameAttribute" class="error"></span> <span id="error-ldap_userNameAttribute" class="error"></span>
</div> </div>
</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"> <div class="control-group">
<label class="control-label" for="ldapMailAttribute">Mail address attribute</label> <label class="control-label" for="ldapMailAttribute">Mail address attribute</label>
<div class="controls"> <div class="controls">

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>

View File

@@ -1,4 +1,4 @@
@(value: String = "", prefix: String = "", mini: Boolean = true, style: String = "")(body: Html) @(value: String = "", prefix: String = "", mini: Boolean = true, style: String = "", right: Boolean = false)(body: Html)
<div class="btn-group"@if(style.nonEmpty){ style="@style"}> <div class="btn-group"@if(style.nonEmpty){ style="@style"}>
<button class="btn dropdown-toggle@if(mini){ btn-mini}" data-toggle="dropdown"> <button class="btn dropdown-toggle@if(mini){ btn-mini}" data-toggle="dropdown">
@if(value.isEmpty){ @if(value.isEmpty){
@@ -11,7 +11,7 @@
} }
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu@if(right){ pull-right}">
@body @body
</ul> </ul>
</div> </div>

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

@@ -6,15 +6,21 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@comments.map { comment => @comments.map { comment =>
@if(comment.action != "close" && comment.action != "reopen"){ @if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div> <div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
<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>
@@ -36,11 +48,11 @@
<div class="small" style="margin-top: 10px; margin-bottom: 10px;"> <div class="small" style="margin-top: 10px; margin-bottom: 10px;">
<span class="label label-info">Merged</span> <span class="label label-info">Merged</span>
@avatar(comment.commentedUserName, 20) @avatar(comment.commentedUserName, 20)
@user(comment.commentedUserName, styleClass="username strong") merged commit <code>@pullreq.map(_.commitIdTo.substring(0, 7))</code> @user(comment.commentedUserName, styleClass="username strong") merged commit <code>@pullreq.map(_.commitIdTo.substring(0, 7))</code> into
@if(pullreq.get.requestUserName == repository.owner){ @if(pullreq.get.requestUserName == repository.owner){
<span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> to <span class="label label-info monospace">@pullreq.map(_.branch)</span> <span class="label label-info monospace">@pullreq.map(_.branch)</span> from <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span>
} else { } else {
<span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span> to <span class="label label-info monospace">@pullreq.map(_.userName):@pullreq.map(_.branch)</span> <span class="label label-info monospace">@pullreq.map(_.userName):@pullreq.map(_.branch)</span> to <span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span>
} }
@datetime(comment.registeredDate) @datetime(comment.registeredDate)
</div> </div>
@@ -63,6 +75,13 @@
@user(comment.commentedUserName, styleClass="username strong") reopened the issue @datetime(comment.registeredDate) @user(comment.commentedUserName, styleClass="username strong") reopened the issue @datetime(comment.registeredDate)
</div> </div>
} }
@if(comment.action == "delete_branch"){
<div class="small issue-comment-action">
<span class="label">Deleted</span>
@avatar(comment.commentedUserName, 20)
@user(comment.commentedUserName, styleClass="username strong") deleted the <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> branch @datetime(comment.registeredDate)
</div>
}
} }
<script> <script>
$(function(){ $(function(){

View File

@@ -2,15 +2,17 @@
@import context._ @import context._
<span id="error-edit-content-@commentId" class="error"></span> <span id="error-edit-content-@commentId" class="error"></span>
<textarea style="width: 680px; height: 100px;" id="edit-content-@commentId">@content</textarea> <textarea style="width: 680px; height: 100px;" id="edit-content-@commentId">@content</textarea>
<input type="button" class="btn btn-small" value="Update Comment"/> <div>
<span class="pull-right"><a class="btn btn-small btn-danger" href="#">Cancel</a></span> <input type="button" id="update-comment-@commentId" class="btn btn-small" value="Update Comment"/>
<input type="button" id="cancel-comment-@commentId" class="btn btn-small btn-danger pull-right" value="Cancel"/>
</div>
<script> <script>
$(function(){ $(function(){
var callback = function(data){ var callback = function(data){
$('#commentContent-@commentId').empty().html(data.content); $('#commentContent-@commentId').empty().html(data.content);
}; };
$('#commentContent-@commentId input.btn').click(function(){ $('#update-comment-@commentId').click(function(){
$.ajax({ $.ajax({
url: '@path/@owner/@repository/issue_comments/edit/@commentId', url: '@path/@owner/@repository/issue_comments/edit/@commentId',
type: 'POST', type: 'POST',
@@ -25,7 +27,7 @@ $(function(){
}); });
}); });
$('#commentContent-@commentId a.btn').click(function(){ $('#cancel-comment-@commentId').click(function(){
$.get('@path/@owner/@repository/issue_comments/_data/@commentId', callback); $.get('@path/@owner/@repository/issue_comments/_data/@commentId', callback);
return false; return false;
}); });

View File

@@ -3,8 +3,10 @@
<span id="error-edit-title" class="error"></span> <span id="error-edit-title" class="error"></span>
<input type="text" style="width: 680px;" id="edit-title" value="@title"/> <input type="text" style="width: 680px;" id="edit-title" value="@title"/>
<textarea style="width: 680px; height: 100px; max-height: 300px;" id="edit-content">@content.getOrElse("")</textarea> <textarea style="width: 680px; height: 100px; max-height: 300px;" id="edit-content">@content.getOrElse("")</textarea>
<input type="button" class="btn btn-small" value="Update Issue"/> <div>
<span class="pull-right"><a class="btn btn-small btn-danger" href="#">Cancel</a></span> <input type="button" id="update" class="btn btn-small" value="Update Issue"/>
<input type="button" id="cancel" class="btn btn-small btn-danger pull-right" value="Cancel"/>
</div>
<script> <script>
$(function(){ $(function(){
$('#edit-content').elastic(); $('#edit-content').elastic();
@@ -14,7 +16,7 @@ $(function(){
$('#issueContent').empty().html(data.content); $('#issueContent').empty().html(data.content);
}; };
$('#issueContent input.btn').click(function(){ $('#update').click(function(){
$.ajax({ $.ajax({
url: '@path/@owner/@repository/issues/edit/@issueId', url: '@path/@owner/@repository/issues/edit/@issueId',
type: 'POST', type: 'POST',
@@ -29,7 +31,7 @@ $(function(){
}); });
}); });
$('#issueContent a.btn').click(function(){ $('#cancel').click(function(){
$.get('@path/@owner/@repository/issues/_data/@issueId', callback); $.get('@path/@owner/@repository/issues/_data/@issueId', callback);
return false; return false;
}); });

View File

@@ -33,51 +33,7 @@
} }
</div> </div>
<hr/> <hr/>
<div style="margin-bottom: 8px;"> @issues.html.labels(issue, issueLabels, labels, hasWritePermission, repository)
<span class="strong">Labels</span>
@if(hasWritePermission){
<div class="pull-right">
@helper.html.dropdown() {
@labels.map { label =>
<li>
<a href="#" class="toggle-label" data-label-id="@label.labelId">
@helper.html.checkicon(issueLabels.exists(_.labelId == label.labelId))
<span class="label" style="background-color: #@label.color;">&nbsp;</span>
@label.labelName
</a>
</li>
}
}
</div>
}
</div>
<ul class="label-list nav nav-pills nav-stacked">
@labellist(issueLabels)
</ul>
</div> </div>
</div> </div>
} }
<script>
$(function(){
$('a.toggle-label').click(function(){
var path, icon;
var i = $(this).children('i');
if(i.hasClass('icon-ok')){
path = 'delete';
icon = 'icon-white';
} else {
path = 'new';
icon = 'icon-ok';
}
$.post('@url(repository)/issues/@issue.issueId/label/' + path,
{
labelId : $(this).data('label-id')
},
function(data){
i.removeClass().addClass(icon);
$('ul.label-list').empty().html(data);
});
return false;
});
});
</script>

View File

@@ -0,0 +1,51 @@
@(issue: model.Issue,
issueLabels: List[model.Label],
labels: List[model.Label],
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import view.helpers._
<div style="margin-bottom: 8px;">
<span class="strong">Labels</span>
@if(hasWritePermission){
<div class="pull-right">
@helper.html.dropdown(right = true) {
@labels.map { label =>
<li>
<a href="#" class="toggle-label" data-label-id="@label.labelId">
@helper.html.checkicon(issueLabels.exists(_.labelId == label.labelId))
<span class="label" style="background-color: #@label.color;">&nbsp;</span>
@label.labelName
</a>
</li>
}
}
</div>
}
</div>
<ul class="label-list nav nav-pills nav-stacked">
@labellist(issueLabels)
</ul>
<script>
$(function(){
$('a.toggle-label').click(function(){
var path, icon;
var i = $(this).children('i');
if(i.hasClass('icon-ok')){
path = 'delete';
icon = 'icon-white';
} else {
path = 'new';
icon = 'icon-ok';
}
$.post('@url(repository)/issues/@issue.issueId/label/' + path,
{
labelId : $(this).data('label-id')
},
function(data){
i.removeClass().addClass(icon);
$('ul.label-list').empty().html(data);
});
return false;
});
});
</script>

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

@@ -1,6 +1,6 @@
@(commits: Seq[Seq[util.JGitUtil.CommitInfo]], @(commits: Seq[Seq[util.JGitUtil.CommitInfo]],
diffs: Seq[util.JGitUtil.DiffInfo], diffs: Seq[util.JGitUtil.DiffInfo],
members: List[String], members: List[(String, String)],
originId: String, originId: String,
forkedId: String, forkedId: String,
sourceId: String, sourceId: String,
@@ -20,25 +20,25 @@
</div> </div>
<div id="compare-edit" style="display: none;"> <div id="compare-edit" style="display: none;">
<a href="#" id="cancel-condition-editing" class="pull-right"><i class="icon-remove-circle"></i></a> <a href="#" id="cancel-condition-editing" class="pull-right"><i class="icon-remove-circle"></i></a>
@helper.html.dropdown(originRepository.owner + "/" + repository.name, "base fork") { @helper.html.dropdown(originRepository.owner + "/" + originRepository.name, "base fork") {
@members.map { member => @members.map { case (owner, name) =>
<li><a href="#" class="origin-owner" data-name="@member">@helper.html.checkicon(member == originRepository.owner) @member/@repository.name</a></li> <li><a href="#" class="origin-owner" data-owner="@owner" data-name="@name">@helper.html.checkicon(owner == originRepository.owner) @owner/@name</a></li>
} }
} }
@helper.html.dropdown(originId, "base") { @helper.html.dropdown(originId, "base") {
@originRepository.branchList.map { branch => @originRepository.branchList.map { branch =>
<li><a href="#" class="origin-branch" data-name="@encodeRefName(branch)">@helper.html.checkicon(branch == originId) @branch</a></li> <li><a href="#" class="origin-branch" data-branch="@encodeRefName(branch)">@helper.html.checkicon(branch == originId) @branch</a></li>
} }
} }
... ...
@helper.html.dropdown(forkedRepository.owner + "/" + repository.name, "head fork") { @helper.html.dropdown(forkedRepository.owner + "/" + forkedRepository.name, "head fork") {
@members.map { member => @members.map { case (owner, name) =>
<li><a href="#" class="forked-owner" data-name="@member">@helper.html.checkicon(member == forkedRepository.owner) @member/@repository.name</a></li> <li><a href="#" class="forked-owner" data-owner="@owner" data-name="@name">@helper.html.checkicon(owner == forkedRepository.owner) @owner/@name</a></li>
} }
} }
@helper.html.dropdown(forkedId, "compare") { @helper.html.dropdown(forkedId, "compare") {
@forkedRepository.branchList.map { branch => @forkedRepository.branchList.map { branch =>
<li><a href="#" class="forked-branch" data-name="@encodeRefName(branch)">@helper.html.checkicon(branch == forkedId) @branch</a></li> <li><a href="#" class="forked-branch" data-branch="@encodeRefName(branch)">@helper.html.checkicon(branch == forkedId) @branch</a></li>
} }
} }
</div> </div>
@@ -49,7 +49,7 @@
</div> </div>
<div id="pull-request-form" class="box" style="display: none;"> <div id="pull-request-form" class="box" style="display: none;">
<div class="box-content"> <div class="box-content">
<form method="POST" action="@path/@originRepository.owner/@repository.name/pulls/new" validate="true"> <form method="POST" action="@path/@originRepository.owner/@originRepository.name/pulls/new" validate="true">
<div style="width: 260px; position: absolute; margin-left: 635px;"> <div style="width: 260px; position: absolute; margin-left: 635px;">
<div class="check-conflict" style="display: none;"> <div class="check-conflict" style="display: none;">
<img src="@assets/common/images/indicator.gif"/> Checking... <img src="@assets/common/images/indicator.gif"/> Checking...
@@ -62,6 +62,7 @@
<input type="hidden" name="targetUserName" value="@originRepository.owner"/> <input type="hidden" name="targetUserName" value="@originRepository.owner"/>
<input type="hidden" name="targetBranch" value="@originId"/> <input type="hidden" name="targetBranch" value="@originId"/>
<input type="hidden" name="requestUserName" value="@forkedRepository.owner"/> <input type="hidden" name="requestUserName" value="@forkedRepository.owner"/>
<input type="hidden" name="requestRepositoryName" value="@forkedRepository.name"/>
<input type="hidden" name="requestBranch" value="@forkedId"/> <input type="hidden" name="requestBranch" value="@forkedId"/>
<input type="hidden" name="commitIdFrom" value="@sourceId"/> <input type="hidden" name="commitIdFrom" value="@sourceId"/>
<input type="hidden" name="commitIdTo" value="@commitId"/> <input type="hidden" name="commitIdTo" value="@commitId"/>
@@ -104,14 +105,16 @@ $(function(){
@if(members.isEmpty){ @if(members.isEmpty){
location.href = '@url(repository)/compare/' + location.href = '@url(repository)/compare/' +
$.trim($('i.icon-ok').parents('a.origin-branch').data('name')) + '...' + $.trim($('i.icon-ok').parents('a.origin-branch').data('branch')) + '...' +
$.trim($('i.icon-ok').parents('a.forked-branch').data('name')); $.trim($('i.icon-ok').parents('a.forked-branch').data('branch'));
} else { } else {
location.href = '@url(repository)/compare/' + location.href = '@path/' +
$.trim($('i.icon-ok').parents('a.origin-owner' ).data('name')) + ':' + $.trim($('i.icon-ok').parents('a.forked-owner' ).data('owner')) + '/' +
$.trim($('i.icon-ok').parents('a.origin-branch').data('name')) + '...' + $.trim($('i.icon-ok').parents('a.forked-owner' ).data('name')) +'/compare/' +
$.trim($('i.icon-ok').parents('a.forked-owner' ).data('name')) + ':' + $.trim($('i.icon-ok').parents('a.origin-owner' ).data('owner')) + ':' +
$.trim($('i.icon-ok').parents('a.forked-branch').data('name')); $.trim($('i.icon-ok').parents('a.origin-branch').data('branch')) + '...' +
$.trim($('i.icon-ok').parents('a.forked-owner' ).data('owner')) + ':' +
$.trim($('i.icon-ok').parents('a.forked-branch').data('branch'));
} }
}); });
@@ -129,15 +132,15 @@ $(function(){
@if(members.isEmpty){ @if(members.isEmpty){
checkConflict( checkConflict(
$.trim($('i.icon-ok').parents('a.origin-branch').data('name')), $.trim($('i.icon-ok').parents('a.origin-branch').data('branch')),
$.trim($('i.icon-ok').parents('a.forked-branch').data('name')) $.trim($('i.icon-ok').parents('a.forked-branch').data('branch'))
); );
} else { } else {
checkConflict( checkConflict(
$.trim($('i.icon-ok').parents('a.origin-owner' ).data('name')) + ":" + $.trim($('i.icon-ok').parents('a.origin-owner' ).data('owner')) + ":" +
$.trim($('i.icon-ok').parents('a.origin-branch').data('name')), $.trim($('i.icon-ok').parents('a.origin-branch').data('branch')),
$.trim($('i.icon-ok').parents('a.forked-owner' ).data('name')) + ":" + $.trim($('i.icon-ok').parents('a.forked-owner' ).data('owner')) + ":" +
$.trim($('i.icon-ok').parents('a.forked-branch').data('name')) $.trim($('i.icon-ok').parents('a.forked-branch').data('branch'))
); );
} }
} }

View File

@@ -1,8 +1,10 @@
@(issue: model.Issue, @(issue: model.Issue,
pullreq: model.PullRequest, pullreq: model.PullRequest,
comments: List[model.IssueComment], comments: List[model.IssueComment],
issueLabels: List[model.Label],
collaborators: List[String], collaborators: List[String],
milestones: List[(model.Milestone, Int, Int)], milestones: List[(model.Milestone, Int, Int)],
labels: List[model.Label],
hasWritePermission: Boolean, hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._ @import context._
@@ -35,6 +37,18 @@
</div> </div>
</div> </div>
} }
@if(hasWritePermission && issue.closed && pullreq.userName == pullreq.requestUserName &&
pullreq.repositoryName == pullreq.requestRepositoryName && repository.branchList.contains(pullreq.requestBranch)){
<div class="box issue-comment-box" style="background-color: #d0eeff;">
<div class="box-content"class="issue-content" style="border: 1px solid #87a8c9; padding: 10px;">
<a href="@url(repository)/pull/@issue.issueId/delete/@pullreq.requestBranch" class="btn btn-info pull-right delete-branch" data-name="@pullreq.requestBranch">Delete branch</a>
<div>
<span class="strong">Pull request successfully merged and closed</span>
</div>
<span class="small muted">You're all set-the <span class="label label-info monospace">@pullreq.requestBranch</span> branch can be safely deleted.</span>
</div>
</div>
}
@issues.html.commentform(issue, hasWritePermission, repository) @issues.html.commentform(issue, hasWritePermission, repository)
</div> </div>
<div class="span2"> <div class="span2">
@@ -51,6 +65,7 @@
<span class="strong">@comments.size</span> @plural(comments.size, "comment") <span class="strong">@comments.size</span> @plural(comments.size, "comment")
</div> </div>
<hr/> <hr/>
@issues.html.labels(issue, issueLabels, labels, hasWritePermission, repository)
</div> </div>
</div> </div>
<script> <script>
@@ -62,8 +77,12 @@ $(function(){
@if(hasWritePermission){ @if(hasWritePermission){
$('.check-conflict').show(); $('.check-conflict').show();
$.get('@url(repository)/pull/@issue.issueId/mergeguide', $.get('@url(repository)/pull/@issue.issueId/mergeguide', function(data){ $('.check-conflict').html(data); });
function(data){ $('.check-conflict').html(data); });
$('.delete-branch').click(function(e){
var branchName = $(e.target).data('name');
return confirm('Are you sure you want to remove the ' + branchName + ' branch?');
});
} }
}); });
</script> </script>

View File

@@ -1,8 +1,10 @@
@(issue: model.Issue, @(issue: model.Issue,
pullreq: model.PullRequest, pullreq: model.PullRequest,
comments: List[model.IssueComment], comments: List[model.IssueComment],
issueLabels: List[model.Label],
collaborators: List[String], collaborators: List[String],
milestones: List[(model.Milestone, Int, Int)], milestones: List[(model.Milestone, Int, Int)],
labels: List[model.Label],
dayByDayCommits: Seq[Seq[util.JGitUtil.CommitInfo]], dayByDayCommits: Seq[Seq[util.JGitUtil.CommitInfo]],
diffs: Seq[util.JGitUtil.DiffInfo], diffs: Seq[util.JGitUtil.DiffInfo],
hasWritePermission: Boolean, hasWritePermission: Boolean,
@@ -17,17 +19,17 @@
@comments.find(_.action == "merge").map{ comment => @comments.find(_.action == "merge").map{ comment =>
<span class="label label-info">Merged</span> <span class="label label-info">Merged</span>
@user(comment.commentedUserName, styleClass="username strong") merged @commits.size @plural(commits.size, "commit") @user(comment.commentedUserName, styleClass="username strong") merged @commits.size @plural(commits.size, "commit")
into <code>@pullreq.requestUserName:@pullreq.requestBranch</code> from <code>@pullreq.userName:@pullreq.branch</code> into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
at @datetime(comment.registeredDate) at @datetime(comment.registeredDate)
}.getOrElse { }.getOrElse {
<span class="label label-important">Closed</span> <span class="label label-important">Closed</span>
@user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit") @user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit")
into <code>@pullreq.requestUserName:@pullreq.requestBranch</code> from <code>@pullreq.userName:@pullreq.branch</code> into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
} }
} else { } else {
<span class="label label-success">Open</span> <span class="label label-success">Open</span>
@user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit") @user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit")
into <code>@pullreq.requestUserName:@pullreq.requestBranch</code> from <code>@pullreq.userName:@pullreq.branch</code> into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
} }
</div> </div>
<ul class="nav nav-tabs" id="pullreq-tab"> <ul class="nav nav-tabs" id="pullreq-tab">
@@ -37,7 +39,7 @@
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane active" id="discussion"> <div class="tab-pane active" id="discussion">
@pulls.html.discussion(issue, pullreq, comments, collaborators, milestones, hasWritePermission, repository) @pulls.html.discussion(issue, pullreq, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
</div> </div>
<div class="tab-pane" id="commits"> <div class="tab-pane" id="commits">
@pulls.html.commits(dayByDayCommits, repository) @pulls.html.commits(dayByDayCommits, repository)

View File

@@ -52,3 +52,33 @@
</tr> </tr>
</table> </table>
} }
<script src="@assets/common/js/jquery.ba-hashchange.js"></script>
<script>
$(window).load(function(){
$(window).hashchange(function(){
updateHighlighting();
}).hashchange();
});
/**
* Hightlight lines which are specified by URL hash.
*/
function updateHighlighting(){
var hash = location.hash;
if(hash.match(/#L\d+(-L\d+)?/)){
$('li.highlight').removeClass('highlight');
var lines = hash.substr(1).split('-');
if(lines.length == 1){
$('#' + lines[0]).addClass('highlight');
$(window).scrollTop($('#' + lines[0]).offset().top - 40);
} else if(lines.length > 1){
var start = parseInt(lines[0].substr(1));
var end = parseInt(lines[1].substr(1));
for(var i = start; i <= end; i++){
$('#L' + i).addClass('highlight');
}
$(window).scrollTop($('#L' + start).offset().top - 40);
}
}
}
</script>

View File

@@ -1,4 +1,5 @@
@(branchInfo: List[(String, java.util.Date)], @(branchInfo: List[(String, java.util.Date)],
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@@ -17,9 +18,9 @@
<tr> <tr>
<td> <td>
<a href="@url(repository)/tree/@encodeRefName(branchName)">@branchName</a> <a href="@url(repository)/tree/@encodeRefName(branchName)">@branchName</a>
@* @if(hasWritePermission && repository.repository.defaultBranch != branchName){
<a href="#" class="btn btn-danger btn-mini">Delete branch</a> <a href="@url(repository)/delete/@encodeRefName(branchName)" class="btn btn-danger btn-mini pull-right delete-branch" data-name="@branchName">Delete branch</a>
*@ }
</td> </td>
<td> <td>
@datetime(latestUpdateDate) @datetime(latestUpdateDate)
@@ -35,4 +36,12 @@
</tr> </tr>
} }
</table> </table>
} }
<script>
$(function(){
$('.delete-branch').click(function(e){
var branchName = $(e.target).data('name');
return confirm('Are you sure you want to remove the ' + branchName + ' branch?');
});
});
</script>

View File

@@ -80,7 +80,7 @@
@readme.map { content => @readme.map { content =>
<div id="readme" class="box"> <div id="readme" class="box">
<div class="box-header">README.md</div> <div class="box-header">README.md</div>
<div class="box-content">@markdown(content, repository, false, false)</div> <div class="box-content markdown-body">@markdown(content, repository, false, false)</div>
</div> </div>
} }
} }

View File

@@ -1,5 +1,5 @@
@(originRepository: Option[service.RepositoryService.RepositoryInfo], @(originRepository: Option[service.RepositoryService.RepositoryInfo],
members: List[String], members: List[(String, String)],
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@@ -23,11 +23,11 @@
} }
(origin) (origin)
</div> </div>
@members.map { owner => @members.map { case (owner, name) =>
<div class="block"> <div class="block">
@avatar(owner, 20) @avatar(owner, 20)
<span@if(repository.owner == owner){ class="highlight"}> <span@if(repository.owner == owner){ class="highlight"}>
<a href="@url(owner)">@owner</a> / <a href="@path/@owner/@repository.name">@repository.name</a> <a href="@url(owner)">@owner</a> / <a href="@path/@owner/@name">@name</a>
</span> </span>
</div> </div>
} }

View File

@@ -18,7 +18,7 @@
} }
</ul> </ul>
@if(!isGroupRepository){ @if(!isGroupRepository){
<form method="POST" action="@url(repository)/settings/collaborators/add" validate="true"> <form method="POST" action="@url(repository)/settings/collaborators/add" validate="true" autocomplete="off">
<div> <div>
<span class="error" id="error-userName"></span> <span class="error" id="error-userName"></span>
</div> </div>

View File

@@ -0,0 +1,44 @@
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Danger Zone", Some(repository)){
@html.header("settings", repository)
@menu("danger", repository){
<div class="box">
<div class="box-header">Danger Zone</div>
<div class="box-content">
<form id="transfer-form" method="post" action="@url(repository)/settings/transfer" validate="true" autocomplete="off">
<fieldset>
<label class="strong">Transfer Ownership</label>
<div>
Transfer this repo to another user or to group.
<div class="pull-right">
@helper.html.account("newOwner", 150)
<input type="submit" class="btn btn-danger" value="Transfer"/>
<div>
<span id="error-newOwner" class="error"></span>
</div>
</div>
</div>
</fieldset>
</form>
<form id="delete-form" method="post" action="@url(repository)/settings/delete">
<fieldset class="margin">
<label class="strong">Delete repository</label>
<div>
Once you delete a repository, there is no going back.
<input type="submit" class="btn btn-danger pull-right" value="Delete this repository"/>
</div>
</fieldset>
</form>
</div>
</div>
}
}
<script>
$(function(){
$('#delete-form').submit(function(){
return confirm('Once you delete a repository, there is no going back.\nAre you sure?');
});
});
</script>

View File

@@ -1,22 +0,0 @@
@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Delete Repository", Some(repository)){
@html.header("settings", repository)
@menu("delete", repository){
<form id="form" method="post" action="@url(repository)/settings/delete">
<h3>Delete repository</h3>
<p>
Once you delete a repository, there is no going back.
</p>
<input type="submit" class="btn btn-danger" value="Delete this repository"/>
</form>
}
}
<script>
$(function(){
$('#form').submit(function(){
return confirm('Once you delete a repository, there is no going back.\nAre you sure?');
});
});
</script>

View File

@@ -14,8 +14,8 @@
<li@if(active=="hooks"){ class="active"}> <li@if(active=="hooks"){ class="active"}>
<a href="@url(repository)/settings/hooks">Service Hooks</a> <a href="@url(repository)/settings/hooks">Service Hooks</a>
</li> </li>
<li@if(active=="delete"){ class="active"}> <li@if(active=="danger"){ class="active"}>
<a href="@url(repository)/settings/delete">Delete Repository</a> <a href="@url(repository)/settings/danger">Danger Zone</a>
</li> </li>
</ul> </ul>
</div> </div>

View File

@@ -10,6 +10,11 @@
<div class="box-header">Settings</div> <div class="box-header">Settings</div>
<div class="box-content"> <div class="box-content">
<fieldset> <fieldset>
<label for="repositoryName" class="strong">Repository Name:</label>
<input type="text" name="repositoryName" id="repositoryName" value="@repository.name"/>
<span id="error-repositoryName" class="error"></span>
</fieldset>
<fieldset class="margin">
<label for="description" class="strong">Description:</label> <label for="description" class="strong">Description:</label>
<input type="text" name="description" id="description" style="width: 600px;" value="@repository.repository.description"/> <input type="text" name="description" id="description" style="width: 600px;" value="@repository.repository.description"/>
</fieldset> </fieldset>

View File

@@ -11,6 +11,13 @@
<listener-class>servlet.SessionCleanupListener</listener-class> <listener-class>servlet.SessionCleanupListener</listener-class>
</listener> </listener>
<!-- ===================================================================== -->
<!-- Automatic migration -->
<!-- ===================================================================== -->
<listener>
<listener-class>servlet.AutoUpdateListener</listener-class>
</listener>
<!-- ===================================================================== --> <!-- ===================================================================== -->
<!-- Scalatra configuration --> <!-- Scalatra configuration -->
<!-- ===================================================================== --> <!-- ===================================================================== -->
@@ -18,6 +25,9 @@
<listener-class>org.scalatra.servlet.ScalatraListener</listener-class> <listener-class>org.scalatra.servlet.ScalatraListener</listener-class>
</listener> </listener>
<!-- ===================================================================== -->
<!-- HTTP interface for Git repositories -->
<!-- ===================================================================== -->
<servlet> <servlet>
<servlet-name>GitRepositoryServlet</servlet-name> <servlet-name>GitRepositoryServlet</servlet-name>
<servlet-class>servlet.GitRepositoryServlet</servlet-class> <servlet-class>servlet.GitRepositoryServlet</servlet-class>
@@ -31,10 +41,6 @@
<!-- ===================================================================== --> <!-- ===================================================================== -->
<!-- H2 database configuration --> <!-- H2 database configuration -->
<!-- ===================================================================== --> <!-- ===================================================================== -->
<listener>
<listener-class>servlet.AutoUpdateListener</listener-class>
</listener>
<context-param> <context-param>
<param-name>db.user</param-name> <param-name>db.user</param-name>
<param-value>sa</param-value> <param-value>sa</param-value>

View File

@@ -480,6 +480,10 @@ pre.blob {
padding: 30px; padding: 30px;
} }
li.highlight {
background-color: #ffb;
}
/****************************************************************************/ /****************************************************************************/
/* Issues */ /* Issues */
/****************************************************************************/ /****************************************************************************/
@@ -706,6 +710,14 @@ ul.collaborator a.remove {
/****************************************************************************/ /****************************************************************************/
/* Markdown */ /* Markdown */
/****************************************************************************/ /****************************************************************************/
div.markdown-body h1 {
border-bottom: 1px solid #ddd;
}
div.markdown-body h2 {
border-bottom: 1px solid #eee;
}
div.markdown-body table { div.markdown-body table {
/*width: 100%;*/ /*width: 100%;*/
margin-bottom: 20px; margin-bottom: 20px;
@@ -838,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

@@ -0,0 +1,390 @@
/*!
* jQuery hashchange event - v1.3 - 7/21/2010
* http://benalman.com/projects/jquery-hashchange-plugin/
*
* Copyright (c) 2010 "Cowboy" Ben Alman
* Dual licensed under the MIT and GPL licenses.
* http://benalman.com/about/license/
*/
// Script: jQuery hashchange event
//
// *Version: 1.3, Last updated: 7/21/2010*
//
// Project Home - http://benalman.com/projects/jquery-hashchange-plugin/
// GitHub - http://github.com/cowboy/jquery-hashchange/
// Source - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.js
// (Minified) - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.min.js (0.8kb gzipped)
//
// About: License
//
// Copyright (c) 2010 "Cowboy" Ben Alman,
// Dual licensed under the MIT and GPL licenses.
// http://benalman.com/about/license/
//
// About: Examples
//
// These working examples, complete with fully commented code, illustrate a few
// ways in which this plugin can be used.
//
// hashchange event - http://benalman.com/code/projects/jquery-hashchange/examples/hashchange/
// document.domain - http://benalman.com/code/projects/jquery-hashchange/examples/document_domain/
//
// About: Support and Testing
//
// Information about what version or versions of jQuery this plugin has been
// tested with, what browsers it has been tested in, and where the unit tests
// reside (so you can test it yourself).
//
// jQuery Versions - 1.2.6, 1.3.2, 1.4.1, 1.4.2
// Browsers Tested - Internet Explorer 6-8, Firefox 2-4, Chrome 5-6, Safari 3.2-5,
// Opera 9.6-10.60, iPhone 3.1, Android 1.6-2.2, BlackBerry 4.6-5.
// Unit Tests - http://benalman.com/code/projects/jquery-hashchange/unit/
//
// About: Known issues
//
// While this jQuery hashchange event implementation is quite stable and
// robust, there are a few unfortunate browser bugs surrounding expected
// hashchange event-based behaviors, independent of any JavaScript
// window.onhashchange abstraction. See the following examples for more
// information:
//
// Chrome: Back Button - http://benalman.com/code/projects/jquery-hashchange/examples/bug-chrome-back-button/
// Firefox: Remote XMLHttpRequest - http://benalman.com/code/projects/jquery-hashchange/examples/bug-firefox-remote-xhr/
// WebKit: Back Button in an Iframe - http://benalman.com/code/projects/jquery-hashchange/examples/bug-webkit-hash-iframe/
// Safari: Back Button from a different domain - http://benalman.com/code/projects/jquery-hashchange/examples/bug-safari-back-from-diff-domain/
//
// Also note that should a browser natively support the window.onhashchange
// event, but not report that it does, the fallback polling loop will be used.
//
// About: Release History
//
// 1.3 - (7/21/2010) Reorganized IE6/7 Iframe code to make it more
// "removable" for mobile-only development. Added IE6/7 document.title
// support. Attempted to make Iframe as hidden as possible by using
// techniques from http://www.paciellogroup.com/blog/?p=604. Added
// support for the "shortcut" format $(window).hashchange( fn ) and
// $(window).hashchange() like jQuery provides for built-in events.
// Renamed jQuery.hashchangeDelay to <jQuery.fn.hashchange.delay> and
// lowered its default value to 50. Added <jQuery.fn.hashchange.domain>
// and <jQuery.fn.hashchange.src> properties plus document-domain.html
// file to address access denied issues when setting document.domain in
// IE6/7.
// 1.2 - (2/11/2010) Fixed a bug where coming back to a page using this plugin
// from a page on another domain would cause an error in Safari 4. Also,
// IE6/7 Iframe is now inserted after the body (this actually works),
// which prevents the page from scrolling when the event is first bound.
// Event can also now be bound before DOM ready, but it won't be usable
// before then in IE6/7.
// 1.1 - (1/21/2010) Incorporated document.documentMode test to fix IE8 bug
// where browser version is incorrectly reported as 8.0, despite
// inclusion of the X-UA-Compatible IE=EmulateIE7 meta tag.
// 1.0 - (1/9/2010) Initial Release. Broke out the jQuery BBQ event.special
// window.onhashchange functionality into a separate plugin for users
// who want just the basic event & back button support, without all the
// extra awesomeness that BBQ provides. This plugin will be included as
// part of jQuery BBQ, but also be available separately.
(function($,window,undefined){
'$:nomunge'; // Used by YUI compressor.
// Reused string.
var str_hashchange = 'hashchange',
// Method / object references.
doc = document,
fake_onhashchange,
special = $.event.special,
// Does the browser support window.onhashchange? Note that IE8 running in
// IE7 compatibility mode reports true for 'onhashchange' in window, even
// though the event isn't supported, so also test document.documentMode.
doc_mode = doc.documentMode,
supports_onhashchange = 'on' + str_hashchange in window && ( doc_mode === undefined || doc_mode > 7 );
// Get location.hash (or what you'd expect location.hash to be) sans any
// leading #. Thanks for making this necessary, Firefox!
function get_fragment( url ) {
url = url || location.href;
return '#' + url.replace( /^[^#]*#?(.*)$/, '$1' );
};
// Method: jQuery.fn.hashchange
//
// Bind a handler to the window.onhashchange event or trigger all bound
// window.onhashchange event handlers. This behavior is consistent with
// jQuery's built-in event handlers.
//
// Usage:
//
// > jQuery(window).hashchange( [ handler ] );
//
// Arguments:
//
// handler - (Function) Optional handler to be bound to the hashchange
// event. This is a "shortcut" for the more verbose form:
// jQuery(window).bind( 'hashchange', handler ). If handler is omitted,
// all bound window.onhashchange event handlers will be triggered. This
// is a shortcut for the more verbose
// jQuery(window).trigger( 'hashchange' ). These forms are described in
// the <hashchange event> section.
//
// Returns:
//
// (jQuery) The initial jQuery collection of elements.
// Allow the "shortcut" format $(elem).hashchange( fn ) for binding and
// $(elem).hashchange() for triggering, like jQuery does for built-in events.
$.fn[ str_hashchange ] = function( fn ) {
return fn ? this.bind( str_hashchange, fn ) : this.trigger( str_hashchange );
};
// Property: jQuery.fn.hashchange.delay
//
// The numeric interval (in milliseconds) at which the <hashchange event>
// polling loop executes. Defaults to 50.
// Property: jQuery.fn.hashchange.domain
//
// If you're setting document.domain in your JavaScript, and you want hash
// history to work in IE6/7, not only must this property be set, but you must
// also set document.domain BEFORE jQuery is loaded into the page. This
// property is only applicable if you are supporting IE6/7 (or IE8 operating
// in "IE7 compatibility" mode).
//
// In addition, the <jQuery.fn.hashchange.src> property must be set to the
// path of the included "document-domain.html" file, which can be renamed or
// modified if necessary (note that the document.domain specified must be the
// same in both your main JavaScript as well as in this file).
//
// Usage:
//
// jQuery.fn.hashchange.domain = document.domain;
// Property: jQuery.fn.hashchange.src
//
// If, for some reason, you need to specify an Iframe src file (for example,
// when setting document.domain as in <jQuery.fn.hashchange.domain>), you can
// do so using this property. Note that when using this property, history
// won't be recorded in IE6/7 until the Iframe src file loads. This property
// is only applicable if you are supporting IE6/7 (or IE8 operating in "IE7
// compatibility" mode).
//
// Usage:
//
// jQuery.fn.hashchange.src = 'path/to/file.html';
$.fn[ str_hashchange ].delay = 50;
/*
$.fn[ str_hashchange ].domain = null;
$.fn[ str_hashchange ].src = null;
*/
// Event: hashchange event
//
// Fired when location.hash changes. In browsers that support it, the native
// HTML5 window.onhashchange event is used, otherwise a polling loop is
// initialized, running every <jQuery.fn.hashchange.delay> milliseconds to
// see if the hash has changed. In IE6/7 (and IE8 operating in "IE7
// compatibility" mode), a hidden Iframe is created to allow the back button
// and hash-based history to work.
//
// Usage as described in <jQuery.fn.hashchange>:
//
// > // Bind an event handler.
// > jQuery(window).hashchange( function(e) {
// > var hash = location.hash;
// > ...
// > });
// >
// > // Manually trigger the event handler.
// > jQuery(window).hashchange();
//
// A more verbose usage that allows for event namespacing:
//
// > // Bind an event handler.
// > jQuery(window).bind( 'hashchange', function(e) {
// > var hash = location.hash;
// > ...
// > });
// >
// > // Manually trigger the event handler.
// > jQuery(window).trigger( 'hashchange' );
//
// Additional Notes:
//
// * The polling loop and Iframe are not created until at least one handler
// is actually bound to the 'hashchange' event.
// * If you need the bound handler(s) to execute immediately, in cases where
// a location.hash exists on page load, via bookmark or page refresh for
// example, use jQuery(window).hashchange() or the more verbose
// jQuery(window).trigger( 'hashchange' ).
// * The event can be bound before DOM ready, but since it won't be usable
// before then in IE6/7 (due to the necessary Iframe), recommended usage is
// to bind it inside a DOM ready handler.
// Override existing $.event.special.hashchange methods (allowing this plugin
// to be defined after jQuery BBQ in BBQ's source code).
special[ str_hashchange ] = $.extend( special[ str_hashchange ], {
// Called only when the first 'hashchange' event is bound to window.
setup: function() {
// If window.onhashchange is supported natively, there's nothing to do..
if ( supports_onhashchange ) { return false; }
// Otherwise, we need to create our own. And we don't want to call this
// until the user binds to the event, just in case they never do, since it
// will create a polling loop and possibly even a hidden Iframe.
$( fake_onhashchange.start );
},
// Called only when the last 'hashchange' event is unbound from window.
teardown: function() {
// If window.onhashchange is supported natively, there's nothing to do..
if ( supports_onhashchange ) { return false; }
// Otherwise, we need to stop ours (if possible).
$( fake_onhashchange.stop );
}
});
// fake_onhashchange does all the work of triggering the window.onhashchange
// event for browsers that don't natively support it, including creating a
// polling loop to watch for hash changes and in IE 6/7 creating a hidden
// Iframe to enable back and forward.
fake_onhashchange = (function(){
var self = {},
timeout_id,
// Remember the initial hash so it doesn't get triggered immediately.
last_hash = get_fragment(),
fn_retval = function(val){ return val; },
history_set = fn_retval,
history_get = fn_retval;
// Start the polling loop.
self.start = function() {
timeout_id || poll();
};
// Stop the polling loop.
self.stop = function() {
timeout_id && clearTimeout( timeout_id );
timeout_id = undefined;
};
// This polling loop checks every $.fn.hashchange.delay milliseconds to see
// if location.hash has changed, and triggers the 'hashchange' event on
// window when necessary.
function poll() {
var hash = get_fragment(),
history_hash = history_get( last_hash );
if ( hash !== last_hash ) {
history_set( last_hash = hash, history_hash );
$(window).trigger( str_hashchange );
} else if ( history_hash !== last_hash ) {
location.href = location.href.replace( /#.*/, '' ) + history_hash;
}
timeout_id = setTimeout( poll, $.fn[ str_hashchange ].delay );
};
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
// vvvvvvvvvvvvvvvvvvv REMOVE IF NOT SUPPORTING IE6/7/8 vvvvvvvvvvvvvvvvvvv
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
$.browser.msie && !supports_onhashchange && (function(){
// Not only do IE6/7 need the "magical" Iframe treatment, but so does IE8
// when running in "IE7 compatibility" mode.
var iframe,
iframe_src;
// When the event is bound and polling starts in IE 6/7, create a hidden
// Iframe for history handling.
self.start = function(){
if ( !iframe ) {
iframe_src = $.fn[ str_hashchange ].src;
iframe_src = iframe_src && iframe_src + get_fragment();
// Create hidden Iframe. Attempt to make Iframe as hidden as possible
// by using techniques from http://www.paciellogroup.com/blog/?p=604.
iframe = $('<iframe tabindex="-1" title="empty"/>').hide()
// When Iframe has completely loaded, initialize the history and
// start polling.
.one( 'load', function(){
iframe_src || history_set( get_fragment() );
poll();
})
// Load Iframe src if specified, otherwise nothing.
.attr( 'src', iframe_src || 'javascript:0' )
// Append Iframe after the end of the body to prevent unnecessary
// initial page scrolling (yes, this works).
.insertAfter( 'body' )[0].contentWindow;
// Whenever `document.title` changes, update the Iframe's title to
// prettify the back/next history menu entries. Since IE sometimes
// errors with "Unspecified error" the very first time this is set
// (yes, very useful) wrap this with a try/catch block.
doc.onpropertychange = function(){
try {
if ( event.propertyName === 'title' ) {
iframe.document.title = doc.title;
}
} catch(e) {}
};
}
};
// Override the "stop" method since an IE6/7 Iframe was created. Even
// if there are no longer any bound event handlers, the polling loop
// is still necessary for back/next to work at all!
self.stop = fn_retval;
// Get history by looking at the hidden Iframe's location.hash.
history_get = function() {
return get_fragment( iframe.location.href );
};
// Set a new history item by opening and then closing the Iframe
// document, *then* setting its location.hash. If document.domain has
// been set, update that as well.
history_set = function( hash, history_hash ) {
var iframe_doc = iframe.document,
domain = $.fn[ str_hashchange ].domain;
if ( hash !== history_hash ) {
// Update Iframe with any initial `document.title` that might be set.
iframe_doc.title = doc.title;
// Opening the Iframe's document after it has been closed is what
// actually adds a history entry.
iframe_doc.open();
// Set document.domain for the Iframe document as well, if necessary.
domain && iframe_doc.write( '<script>document.domain="' + domain + '"</script>' );
iframe_doc.close();
// Update the Iframe's hash, for great justice.
iframe.location.hash = hash;
}
};
})();
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// ^^^^^^^^^^^^^^^^^^^ REMOVE IF NOT SUPPORTING IE6/7/8 ^^^^^^^^^^^^^^^^^^^
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
return self;
})();
})(jQuery,this);

View File

@@ -12,7 +12,7 @@ q,"'\"`"]):d.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?
s+")*(?:\\x5D|$))+/")+")")])}(b=a.types)&&g.push(["typ",b]);b=(""+a.keywords).replace(/^ | $/g,"");b.length&&g.push(["kwd",RegExp("^(?:"+b.replace(/[\s,]+/g,"|")+")\\b"),q]);d.push(["pln",/^\s+/,q," \r\n\t\u00a0"]);b="^.[^\\s\\w.$@'\"`/\\\\]*";a.regexLiterals&&(b+="(?!s*/)");g.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/, s+")*(?:\\x5D|$))+/")+")")])}(b=a.types)&&g.push(["typ",b]);b=(""+a.keywords).replace(/^ | $/g,"");b.length&&g.push(["kwd",RegExp("^(?:"+b.replace(/[\s,]+/g,"|")+")\\b"),q]);d.push(["pln",/^\s+/,q," \r\n\t\u00a0"]);b="^.[^\\s\\w.$@'\"`/\\\\]*";a.regexLiterals&&(b+="(?!s*/)");g.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,
q],["pun",RegExp(b),q]);return C(d,g)}function J(a,d,g){function b(a){var c=a.nodeType;if(c==1&&!x.test(a.className))if("br"===a.nodeName)s(a),a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)b(a);else if((c==3||c==4)&&g){var d=a.nodeValue,i=d.match(m);if(i)c=d.substring(0,i.index),a.nodeValue=c,(d=d.substring(i.index+i[0].length))&&a.parentNode.insertBefore(j.createTextNode(d),a.nextSibling),s(a),c||a.parentNode.removeChild(a)}}function s(a){function b(a,c){var d= q],["pun",RegExp(b),q]);return C(d,g)}function J(a,d,g){function b(a){var c=a.nodeType;if(c==1&&!x.test(a.className))if("br"===a.nodeName)s(a),a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)b(a);else if((c==3||c==4)&&g){var d=a.nodeValue,i=d.match(m);if(i)c=d.substring(0,i.index),a.nodeValue=c,(d=d.substring(i.index+i[0].length))&&a.parentNode.insertBefore(j.createTextNode(d),a.nextSibling),s(a),c||a.parentNode.removeChild(a)}}function s(a){function b(a,c){var d=
c?a.cloneNode(!1):a,e=a.parentNode;if(e){var e=b(e,1),g=a.nextSibling;e.appendChild(d);for(var i=g;i;i=g)g=i.nextSibling,e.appendChild(i)}return d}for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),d;(d=a.parentNode)&&d.nodeType===1;)a=d;c.push(a)}for(var x=/(?:^|\s)nocode(?:\s|$)/,m=/\r\n?|\n/,j=a.ownerDocument,k=j.createElement("li");a.firstChild;)k.appendChild(a.firstChild);for(var c=[k],i=0;i<c.length;++i)b(c[i]);d===(d|0)&&c[0].setAttribute("value",d);var r=j.createElement("ol"); c?a.cloneNode(!1):a,e=a.parentNode;if(e){var e=b(e,1),g=a.nextSibling;e.appendChild(d);for(var i=g;i;i=g)g=i.nextSibling,e.appendChild(i)}return d}for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),d;(d=a.parentNode)&&d.nodeType===1;)a=d;c.push(a)}for(var x=/(?:^|\s)nocode(?:\s|$)/,m=/\r\n?|\n/,j=a.ownerDocument,k=j.createElement("li");a.firstChild;)k.appendChild(a.firstChild);for(var c=[k],i=0;i<c.length;++i)b(c[i]);d===(d|0)&&c[0].setAttribute("value",d);var r=j.createElement("ol");
r.className="linenums";for(var d=Math.max(0,d-1|0)||0,i=0,n=c.length;i<n;++i)k=c[i],k.className="L"+(i+d)%10,k.firstChild||k.appendChild(j.createTextNode("\u00a0")),r.appendChild(k);a.appendChild(r)}function p(a,d){for(var g=d.length;--g>=0;){var b=d[g];F.hasOwnProperty(b)?D.console&&console.warn("cannot override language handler %s",b):F[b]=a}}function I(a,d){if(!a||!F.hasOwnProperty(a))a=/^\s*</.test(d)?"default-markup":"default-code";return F[a]}function K(a){var d=a.h;try{var g=T(a.c,a.i),b=g.a; r.className="linenums";for(var d=Math.max(0,d-1|0)||0,i=0,n=c.length;i<n;++i)k=c[i],k.id="L"+(i+1),k.className="L"+(i+d)%10,k.firstChild||k.appendChild(j.createTextNode("\u00a0")),r.appendChild(k);a.appendChild(r)}function p(a,d){for(var g=d.length;--g>=0;){var b=d[g];F.hasOwnProperty(b)?D.console&&console.warn("cannot override language handler %s",b):F[b]=a}}function I(a,d){if(!a||!F.hasOwnProperty(a))a=/^\s*</.test(d)?"default-markup":"default-code";return F[a]}function K(a){var d=a.h;try{var g=T(a.c,a.i),b=g.a;
a.a=b;a.d=g.d;a.e=0;I(d,b)(a);var s=/\bMSIE\s(\d+)/.exec(navigator.userAgent),s=s&&+s[1]<=8,d=/\n/g,x=a.a,m=x.length,g=0,j=a.d,k=j.length,b=0,c=a.g,i=c.length,r=0;c[i]=m;var n,e;for(e=n=0;e<i;)c[e]!==c[e+2]?(c[n++]=c[e++],c[n++]=c[e++]):e+=2;i=n;for(e=n=0;e<i;){for(var p=c[e],w=c[e+1],t=e+2;t+2<=i&&c[t+1]===w;)t+=2;c[n++]=p;c[n++]=w;e=t}c.length=n;var f=a.c,h;if(f)h=f.style.display,f.style.display="none";try{for(;b<k;){var l=j[b+2]||m,B=c[r+2]||m,t=Math.min(l,B),A=j[b+1],G;if(A.nodeType!==1&&(G=x.substring(g, a.a=b;a.d=g.d;a.e=0;I(d,b)(a);var s=/\bMSIE\s(\d+)/.exec(navigator.userAgent),s=s&&+s[1]<=8,d=/\n/g,x=a.a,m=x.length,g=0,j=a.d,k=j.length,b=0,c=a.g,i=c.length,r=0;c[i]=m;var n,e;for(e=n=0;e<i;)c[e]!==c[e+2]?(c[n++]=c[e++],c[n++]=c[e++]):e+=2;i=n;for(e=n=0;e<i;){for(var p=c[e],w=c[e+1],t=e+2;t+2<=i&&c[t+1]===w;)t+=2;c[n++]=p;c[n++]=w;e=t}c.length=n;var f=a.c,h;if(f)h=f.style.display,f.style.display="none";try{for(;b<k;){var l=j[b+2]||m,B=c[r+2]||m,t=Math.min(l,B),A=j[b+1],G;if(A.nodeType!==1&&(G=x.substring(g,
t))){s&&(G=G.replace(d,"\r"));A.nodeValue=G;var L=A.ownerDocument,o=L.createElement("span");o.className=c[r+1];var v=A.parentNode;v.replaceChild(o,A);o.appendChild(A);g<l&&(j[b+1]=A=L.createTextNode(x.substring(t,l)),v.insertBefore(A,o.nextSibling))}g=t;g>=l&&(b+=2);g>=B&&(r+=2)}}finally{if(f)f.style.display=h}}catch(u){D.console&&console.log(u&&u.stack||u)}}var D=window,y=["break,continue,do,else,for,if,return,while"],E=[[y,"auto,case,char,const,default,double,enum,extern,float,goto,inline,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], t))){s&&(G=G.replace(d,"\r"));A.nodeValue=G;var L=A.ownerDocument,o=L.createElement("span");o.className=c[r+1];var v=A.parentNode;v.replaceChild(o,A);o.appendChild(A);g<l&&(j[b+1]=A=L.createTextNode(x.substring(t,l)),v.insertBefore(A,o.nextSibling))}g=t;g>=l&&(b+=2);g>=B&&(r+=2)}}finally{if(f)f.style.display=h}}catch(u){D.console&&console.log(u&&u.stack||u)}}var D=window,y=["break,continue,do,else,for,if,return,while"],E=[[y,"auto,case,char,const,default,double,enum,extern,float,goto,inline,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"],
"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],M=[E,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,delegate,dynamic_cast,explicit,export,friend,generic,late_check,mutable,namespace,nullptr,property,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],N=[E,"abstract,assert,boolean,byte,extends,final,finally,implements,import,instanceof,interface,null,native,package,strictfp,super,synchronized,throws,transient"], "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],M=[E,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,delegate,dynamic_cast,explicit,export,friend,generic,late_check,mutable,namespace,nullptr,property,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],N=[E,"abstract,assert,boolean,byte,extends,final,finally,implements,import,instanceof,interface,null,native,package,strictfp,super,synchronized,throws,transient"],

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

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

@@ -0,0 +1,28 @@
package view
import org.specs2.mutable._
class GitBucketHtmlSerializerSpec extends Specification {
import GitBucketHtmlSerializer._
"generateAnchorName" should {
"convert whitespace characters to hyphens" in {
val before = "foo bar baz"
val after = generateAnchorName(before)
after mustEqual "foo-bar-baz"
}
"normalize characters with diacritics" in {
val before = "Dónde estará mi vida"
val after = generateAnchorName(before)
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 "foo%21bar%40baz%3e9000"
}
}
}