Compare commits

...

65 Commits
1.5 ... 1.6

Author SHA1 Message Date
Naoki Takezoe
9d71d39917 Update README.md 2013-09-29 16:31:12 +09:00
takezoe
5430564065 (refs #105)Specify suitable Content-Type header for downloaded file. 2013-09-29 16:27:40 +09:00
shimamoto
54bc8c16d8 (refs #100) Fix bug that can't get ServletContext in the future block. 2013-09-28 21:03:55 +09:00
takezoe
0affdb6ad0 Bug fix caused by path splitting. 2013-09-27 14:33:27 +09:00
takezoe
532978522a Fix logback configuration. 2013-09-27 14:07:20 +09:00
Naoki Takezoe
05a9a0b45c Update README.md 2013-09-27 11:32:15 +09:00
Naoki Takezoe
24f8ad11ad Update README.md 2013-09-27 11:28:05 +09:00
Naoki Takezoe
ce943a0e6c Update README.md 2013-09-27 11:26:06 +09:00
takezoe
204c0cd0f8 (refs #96)Add --port and --prefix option. 2013-09-27 03:05:05 +09:00
takezoe
c213008f1c (refs #96)Small fix for build.xml. 2013-09-27 02:45:00 +09:00
takezoe
e6ad069509 (refs #96)Improve Jetty embedding process. 2013-09-27 02:43:22 +09:00
takezoe
38c7e3cdf8 (refs #96)Add build.xml which makes an executable war file. 2013-09-26 20:17:33 +09:00
takezoe
2be79f6590 Replace trace log with debug log. 2013-09-26 12:02:34 +09:00
takezoe
2f7125b6c0 Add trace log to WebHookService to check future execution. 2013-09-25 13:12:35 +09:00
takezoe
bb03a6fc9b (refs #94)The merge-guide is separated as HTML fragment and retrieve them by Ajax. 2013-09-23 13:25:06 +09:00
takezoe
7b774aee1a Use helper.html.dropdown() instead of the direct Bootstrap use. 2013-09-23 03:13:21 +09:00
takezoe
d53619c247 Small fix. 2013-09-23 02:14:31 +09:00
takezoe
d34118bdfd Define request attribute keys. 2013-09-23 02:03:10 +09:00
takezoe
c57bc487a3 Define session keys. 2013-09-23 00:51:57 +09:00
takezoe
296fc9a3df Improve session handling. 2013-09-23 00:18:38 +09:00
takezoe
fd8b5780f3 Use ControlUtil. 2013-09-22 19:28:14 +09:00
takezoe
602b6c635a Add RichRequest which extends HttpServletRequest. 2013-09-22 14:25:50 +09:00
takezoe
a79180699e Generalize the account completion field. 2013-09-22 13:35:05 +09:00
takezoe
e9901a8abf Generalize the commit list in the pull request. 2013-09-22 04:19:41 +09:00
takezoe
4e63d64c13 Generalize the file index of diff. 2013-09-22 04:05:51 +09:00
takezoe
4261b7adbe Use .strong instead of <strong>. 2013-09-22 03:27:18 +09:00
takezoe
f30c9f6171 Use ControlUtil. 2013-09-22 01:24:04 +09:00
takezoe
c00b704843 Use ControlUtil#using() to handle RevWalk. 2013-09-21 22:21:59 +09:00
takezoe
e89b2020a3 Use ControlUtil. 2013-09-21 22:13:15 +09:00
Naoki Takezoe
18ca3cbd80 Update README.md 2013-09-20 13:48:37 +09:00
takezoe
062d6cd066 Add ControlUtil. 2013-09-19 18:53:50 +09:00
takezoe
b4dd067d61 Introduce ControlUtil which provides control facilities such as using() or defining(). 2013-09-19 18:53:14 +09:00
takezoe
fd22e2911a Remove debug code. 2013-09-19 13:26:33 +09:00
takezoe
73d9e69e43 (refs #74)Small fix for test hook. 2013-09-19 02:40:07 +09:00
takezoe
7e4c29f4cf (refs #74)Remove an auxiliary constructor from case class because json4s can't serialize correctly if case class have that. 2013-09-19 00:47:46 +09:00
takezoe
32672262ef Merge branch 'master' of https://github.com/takezoe/gitbucket 2013-09-18 20:13:17 +09:00
takezoe
3c865ea20b (refs #74)Remove an unnecessary action and add TODO. 2013-09-18 20:12:47 +09:00
takezoe
d8698d02b7 (refs #74)Add "Test Hook" button. 2013-09-18 20:10:53 +09:00
shimamoto
d5b47e5adb Merge branch 'master' of https://github.com/takezoe/gitbucket.git 2013-09-18 19:22:25 +09:00
shimamoto
accb1cf2ab Refactoring. 2013-09-18 19:21:56 +09:00
takezoe
aa8da1b046 Fix TODO. 2013-09-18 15:28:59 +09:00
takezoe
c52ed32949 Fix whitespaces. 2013-09-18 15:27:25 +09:00
takezoe
ec6f4ff734 Fix typo. 2013-09-18 14:30:41 +09:00
takezoe
06b0dbf2e5 Remove TODO. 2013-09-18 14:24:15 +09:00
takezoe
98d24248c2 Add ExecutionContext for Future. 2013-09-14 18:34:05 +09:00
takezoe
cec1dc98a9 (refs #74)Web hook request is sent asynchronously. 2013-09-14 17:43:06 +09:00
takezoe
36115734bb (refs #74)Web hook is completed. 2013-09-14 17:14:37 +09:00
takezoe
c1eccd391d (refs #74)JSON conversion test. 2013-09-13 21:58:50 +09:00
takezoe
7fe86fcdb2 Merge branch 'webhook' of https://github.com/takezoe/gitbucket into webhook 2013-09-13 19:07:09 +09:00
takezoe
7f81ec52c1 (refs #74)JSON conversion test. 2013-09-13 19:06:45 +09:00
takezoe
7c269de39b (refs #74)Implementing conversion of web hook payload. 2013-09-13 03:24:34 +09:00
takezoe
aa9e34e992 (refs #74)Added case classes for payload of web hook. 2013-09-12 12:57:07 +09:00
takezoe
4d0ab514fb (refs #74)Remove web hook URL is available. 2013-09-12 08:41:26 +09:00
takezoe
9d526b32e0 Delete from WEB_HOOK before deleting repository. 2013-09-12 01:38:52 +09:00
takezoe
90a83c5c64 Merge branch 'master' into webhook 2013-09-12 01:34:31 +09:00
takezoe
e6e5cc67d5 Delete from PULL_REQUEST before deleting repository. 2013-09-11 18:05:34 +09:00
takezoe
4a6eb95474 Fix redirect path to the context root. 2013-09-11 03:53:50 +09:00
takezoe
7bce8cf3b6 Fix redirect path after sign in. 2013-09-11 03:40:55 +09:00
takezoe
4d1605ded2 (refs #74)Add web hook URL addition. 2013-09-06 02:32:51 +09:00
Naoki Takezoe
2bec2cfa93 Merge pull request #95 from kaakaa/fix-activity-bug
Fix a problem in making link to commit in activities
2013-09-05 09:24:32 -07:00
kaakaa
ff07872a3d Fix a problem in making link to commit in activities 2013-09-05 22:34:51 +09:00
takezoe
35733cd82e Merge branch 'master' into webhook 2013-09-05 14:53:58 +09:00
takezoe
38df990033 Merge branch 'master' into webhook 2013-09-05 02:35:58 +09:00
takezoe
940e2f4759 (refs #74)Add WEB_HOOK table. 2013-09-02 18:17:28 +09:00
takezoe
6fe65c76b1 (refs #74)Add the web hook configuration page. 2013-08-28 13:21:51 +09:00
87 changed files with 1942 additions and 1544 deletions

View File

@@ -6,7 +6,7 @@ GitBucket is the easily installable Github clone written with Scala.
The current version of GitBucket provides a basic features below: The current version of GitBucket provides a basic features below:
- Public / Private Git repository (http access only) - Public / Private Git repository (http access only)
- Repository viewer (some advanced features are not implemented) - Repository viewer (some advanced features such as online file editing are not implemented)
- Repository search (Code and Issues) - Repository search (Code and Issues)
- Wiki - Wiki
- Issues - Issues
@@ -15,9 +15,13 @@ The current version of GitBucket provides a basic features below:
- Activity timeline - Activity timeline
- User management (for Administrators) - User management (for Administrators)
- Group (like Organization in Github) - Group (like Organization in Github)
- LDAP integration
- Gravatar support
Following features are not implemented, but we will make them in the future release! Following features are not implemented, but we will make them in the future release!
- File editing in repository viewer
- Comment for the changeset
- Network graph - Network graph
- Statics - Statics
- Watch / Star - Watch / Star
@@ -33,10 +37,22 @@ Installation
The default administrator account is **root** and password is **root**. The default administrator account is **root** and password is **root**.
To upgrade GitBucket, only replace gitbucket.war. (Since 1.6) 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]
- --prefix=[CONTEXTPATH]
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.
Release Notes Release Notes
-------- --------
### 1.6 - COMMING SOON!
- Web hook.
- Performance improvement for pull request.
- Executable war file.
- Specify suitable Content-Type for downloaded files in the repository viewer.
- Fixed some bugs.
### 1.5 - 4 Sep 2013 ### 1.5 - 4 Sep 2013
- Fork and pull request. - Fork and pull request.
- LDAP authentication. - LDAP authentication.

55
build.xml Normal file
View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" ?>
<project name="gitbucket" default="all" basedir=".">
<property name="target.dir" value="target"/>
<property name="embed.classes.dir" value="${target.dir}/embed-classes"/>
<property name="jetty.dir" value="embed-jetty"/>
<property name="scala.version" value="2.10"/>
<property name="gitbucket.version" value="0.0.1"/>
<property name="jetty.version" value="8.1.8.v20121106"/>
<property name="servlet.version" value="3.0.0.v201112011016"/>
<target name="clean">
<delete dir="${embed.classes.dir}"/>
<delete file="${target.dir}/scala-${scala.version}/gitbucket.war"/>
</target>
<target name="war" depends="clean">
<exec command="sbt.bat clean package"/>
</target>
<target name="embed" depends="war">
<mkdir dir="${embed.classes.dir}"/>
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/javax.servlet-${servlet.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-continuation-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-http-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-io-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-security-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-server-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-servlet-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-util-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-webapp-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-xml-${jetty.version}.jar" />
<zip destfile="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
basedir="${embed.classes.dir}"
update = "true"
includes="javax/**,org/**"/>
<zip destfile="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
basedir="${target.dir}/scala-${scala.version}/classes"
update = "true"
includes="JettyLauncher.class"/>
</target>
<target name="rename" depends="embed">
<rename src="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
dest="${target.dir}/scala-${scala.version}/gitbucket.war"/>
</target>
<target name="all" depends="rename">
</target>
</project>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -36,14 +36,16 @@ object MyBuild extends Build {
"org.pegdown" % "pegdown" % "1.3.0", "org.pegdown" % "pegdown" % "1.3.0",
"org.apache.commons" % "commons-compress" % "1.5", "org.apache.commons" % "commons-compress" % "1.5",
"org.apache.commons" % "commons-email" % "1.3.1", "org.apache.commons" % "commons-email" % "1.3.1",
"org.apache.httpcomponents" % "httpclient" % "4.2.5",
"com.typesafe.slick" %% "slick" % "1.0.1", "com.typesafe.slick" %% "slick" % "1.0.1",
"com.novell.ldap" % "jldap" % "2009-10-07", "com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.3.171", "com.h2database" % "h2" % "1.3.171",
"ch.qos.logback" % "logback-classic" % "1.0.6" % "runtime", "ch.qos.logback" % "logback-classic" % "1.0.6" % "runtime",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container", "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"))
), ),
EclipseKeys.withSource := true EclipseKeys.withSource := true,
packageOptions += Package.MainClass("JettyLauncher")
) ++ seq(Twirl.settings: _*) ) ++ seq(Twirl.settings: _*)
) )
} }

View File

@@ -0,0 +1,39 @@
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;
import java.net.URL;
import java.security.ProtectionDomain;
public class JettyLauncher {
public static void main(String[] args) throws Exception {
int port = 8080;
String contextPath = "/";
for(String arg: args){
if(arg.startsWith("--") && arg.contains("=")){
String[] dim = arg.split("=");
if(dim.length >= 2){
if(dim[0].equals("--port")){
port = Integer.parseInt(dim[1]);
} else if(dim[0].equals("--prefix")){
contextPath = dim[1];
}
}
}
}
Server server = new Server(port);
WebAppContext context = new WebAppContext();
ProtectionDomain domain = JettyLauncher.class.getProtectionDomain();
URL location = domain.getCodeSource().getLocation();
context.setContextPath(contextPath);
context.setDescriptor(location.toExternalForm() + "/WEB-INF/web.xml");
context.setServer(server);
context.setWar(location.toExternalForm());
server.setHandler(context);
server.start();
server.join();
}
}

View File

@@ -1,4 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<configuration> <configuration>
<logger name="scala.slick" level="INFO" /> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
<!--
<logger name="service.WebHookService" level="DEBUG" />
<logger name="servlet" level="DEBUG" />
-->
</configuration> </configuration>

View File

@@ -0,0 +1,8 @@
CREATE TABLE WEB_HOOK (
USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL,
URL VARCHAR(200) NOT NULL
);
ALTER TABLE WEB_HOOK ADD CONSTRAINT IDX_WEB_HOOK_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, URL);
ALTER TABLE WEB_HOOK ADD CONSTRAINT IDX_WEB_HOOK_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);

View File

@@ -1,7 +1,9 @@
package app package app
import _root_.util.Directory._ import _root_.util.Directory._
import _root_.util.{FileUtil, Validations} import _root_.util.Implicits._
import _root_.util.ControlUtil._
import _root_.util.{FileUtil, Validations, Keys}
import org.scalatra._ import org.scalatra._
import org.scalatra.json._ import org.scalatra.json._
import org.json4s._ import org.json4s._
@@ -32,7 +34,7 @@ abstract class ControllerBase extends ScalatraFilter
val path = httpRequest.getRequestURI.substring(context.length) val path = httpRequest.getRequestURI.substring(context.length)
if(path.startsWith("/console/")){ if(path.startsWith("/console/")){
val account = httpRequest.getSession.getAttribute("LOGIN_ACCOUNT").asInstanceOf[Account] val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
if(account == null){ if(account == null){
// Redirect to login form // Redirect to login form
httpResponse.sendRedirect(context + "/signin?" + path) httpResponse.sendRedirect(context + "/signin?" + path)
@@ -57,56 +59,47 @@ abstract class ControllerBase extends ScalatraFilter
*/ */
implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL, request) implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL, request)
private def currentURL: String = { private def currentURL: String = defining(request.getQueryString){ queryString =>
val queryString = request.getQueryString
request.getRequestURI + (if(queryString != null) "?" + queryString else "") request.getRequestURI + (if(queryString != null) "?" + queryString else "")
} }
private def LoginAccount: Option[Account] = { private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount)
session.get("LOGIN_ACCOUNT") match {
case Some(x: Account) => Some(x)
case _ => None
}
}
def ajaxGet(path : String)(action : => Any) : Route = { def ajaxGet(path : String)(action : => Any) : Route =
super.get(path){ super.get(path){
request.setAttribute("AJAX", "true") request.setAttribute(Keys.Request.Ajax, "true")
action action
} }
}
override def ajaxGet[T](path : String, form : MappingValueType[T])(action : T => Any) : Route = { override def ajaxGet[T](path : String, form : MappingValueType[T])(action : T => Any) : Route =
super.ajaxGet(path, form){ form => super.ajaxGet(path, form){ form =>
request.setAttribute("AJAX", "true") request.setAttribute(Keys.Request.Ajax, "true")
action(form) action(form)
} }
}
def ajaxPost(path : String)(action : => Any) : Route = { def ajaxPost(path : String)(action : => Any) : Route =
super.post(path){ super.post(path){
request.setAttribute("AJAX", "true") request.setAttribute(Keys.Request.Ajax, "true")
action action
} }
}
override def ajaxPost[T](path : String, form : MappingValueType[T])(action : T => Any) : Route = { override def ajaxPost[T](path : String, form : MappingValueType[T])(action : T => Any) : Route =
super.ajaxPost(path, form){ form => super.ajaxPost(path, form){ form =>
request.setAttribute("AJAX", "true") request.setAttribute(Keys.Request.Ajax, "true")
action(form) action(form)
} }
}
protected def NotFound() = { protected def NotFound() =
if(request.getAttribute("AJAX") == null){ if(request.hasAttribute(Keys.Request.Ajax)){
org.scalatra.NotFound(html.error("Not Found"))
} else {
org.scalatra.NotFound() org.scalatra.NotFound()
} else {
org.scalatra.NotFound(html.error("Not Found"))
} }
}
protected def Unauthorized()(implicit context: app.Context) = { protected def Unauthorized()(implicit context: app.Context) =
if(request.getAttribute("AJAX") == null){ if(request.hasAttribute(Keys.Request.Ajax)){
org.scalatra.Unauthorized()
} else {
if(context.loginAccount.isDefined){ if(context.loginAccount.isDefined){
org.scalatra.Unauthorized(redirect("/")) org.scalatra.Unauthorized(redirect("/"))
} else { } else {
@@ -116,13 +109,9 @@ abstract class ControllerBase extends ScalatraFilter
org.scalatra.Unauthorized(redirect("/signin?redirect=" + currentURL)) org.scalatra.Unauthorized(redirect("/signin?redirect=" + currentURL))
} }
} }
} else {
org.scalatra.Unauthorized()
} }
}
protected def baseUrl = { protected def baseUrl = defining(request.getRequestURL.toString){ url =>
val url = request.getRequestURL.toString
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length)) url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
} }
@@ -133,12 +122,10 @@ abstract class ControllerBase extends ScalatraFilter
*/ */
case class Context(path: String, loginAccount: Option[Account], currentUrl: String, request: HttpServletRequest){ case class Context(path: String, loginAccount: Option[Account], currentUrl: String, request: HttpServletRequest){
def redirectUrl = { def redirectUrl = if(request.getParameter("redirect") != null){
if(request.getParameter("redirect") != null){ request.getParameter("redirect")
request.getParameter("redirect") } else {
} else { currentUrl
currentUrl
}
} }
/** /**
@@ -147,13 +134,14 @@ case class Context(path: String, loginAccount: Option[Account], currentUrl: Stri
* If object has not been cached with the specified key then retrieves by given action. * If object has not been cached with the specified key then retrieves by given action.
* Cached object are available during a request. * Cached object are available during a request.
*/ */
def cache[A](key: String)(action: => A): A = { def cache[A](key: String)(action: => A): A =
Option(request.getAttribute("cache." + key).asInstanceOf[A]).getOrElse { defining(Keys.Request.Cache(key)){ cacheKey =>
val newObject = action Option(request.getAttribute(cacheKey).asInstanceOf[A]).getOrElse {
request.setAttribute("cache." + key, newObject) val newObject = action
newObject request.setAttribute(cacheKey, newObject)
newObject
}
} }
}
} }
@@ -163,7 +151,7 @@ case class Context(path: String, loginAccount: Option[Account], currentUrl: Stri
trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase { trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase {
self: AccountService => self: AccountService =>
protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit = { protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit =
if(clearImage){ if(clearImage){
getAccountByUserName(userName).flatMap(_.image).map { image => getAccountByUserName(userName).flatMap(_.image).map { image =>
new java.io.File(getUserUploadDir(userName), image).delete() new java.io.File(getUserUploadDir(userName), image).delete()
@@ -179,7 +167,6 @@ trait AccountManagementControllerBase extends ControllerBase with FileUploadCont
updateAvatarImage(userName, Some(filename)) updateAvatarImage(userName, Some(filename))
} }
} }
}
protected def uniqueUserName: Constraint = new Constraint(){ protected def uniqueUserName: Constraint = new Constraint(){
override def validate(name: String, value: String): Option[String] = override def validate(name: String, value: String): Option[String] =
@@ -215,12 +202,7 @@ trait FileUploadControllerBase {
def removeTemporaryFiles()(implicit session: HttpSession): Unit = def removeTemporaryFiles()(implicit session: HttpSession): Unit =
FileUtils.deleteDirectory(TemporaryDir) FileUtils.deleteDirectory(TemporaryDir)
def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] = { def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] =
val filename = Option(session.getAttribute("upload_" + fileId).asInstanceOf[String]) session.getAndRemove[String](Keys.Session.Upload(fileId))
if(filename.isDefined){
session.removeAttribute("upload_" + fileId)
}
filename
}
} }

View File

@@ -1,6 +1,7 @@
package app package app
import util.Directory._ import util.Directory._
import util.ControlUtil._
import util._ import util._
import service._ import service._
import java.io.File import java.io.File
@@ -149,7 +150,7 @@ trait CreateRepositoryControllerBase extends ControllerBase {
getWikiRepositoryDir(loginUserName, repository.name)) getWikiRepositoryDir(loginUserName, repository.name))
// insert commit id // insert commit id
JGitUtil.withGit(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 =>

View File

@@ -1,7 +1,8 @@
package app package app
import service._ import service._
import util.UsersAuthenticator import util.{UsersAuthenticator, Keys}
import util.Implicits._
class DashboardController extends DashboardControllerBase class DashboardController extends DashboardControllerBase
with IssuesService with PullRequestService with RepositoryService with AccountService with IssuesService with PullRequestService with RepositoryService with AccountService
@@ -42,12 +43,10 @@ trait DashboardControllerBase extends ControllerBase {
import IssuesService._ import IssuesService._
// condition // condition
val sessionKey = "dashboard/issues" val condition = session.putAndGet(Keys.Session.DashboardIssues,
val condition = if(request.getQueryString == null) if(request.hasQueryString) IssueSearchCondition(request)
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition] else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition())
else IssueSearchCondition(request) )
session.put(sessionKey, condition)
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name) val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
@@ -75,15 +74,10 @@ trait DashboardControllerBase extends ControllerBase {
import PullRequestService._ import PullRequestService._
// condition // condition
val sessionKey = "dashboard/pulls" val condition = session.putAndGet(Keys.Session.DashboardPulls, {
val condition = { if(request.hasQueryString) IssueSearchCondition(request)
if(request.getQueryString == null) else session.getAs[IssueSearchCondition](Keys.Session.DashboardPulls).getOrElse(IssueSearchCondition())
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition] }.copy(repo = repository))
else
IssueSearchCondition(request)
}.copy(repo = repository)
session.put(sessionKey, condition)
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name) val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)

View File

@@ -1,6 +1,7 @@
package app package app
import util.{FileUtil} import _root_.util.{Keys, FileUtil}
import util.ControlUtil._
import org.scalatra._ import org.scalatra._
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport} import org.scalatra.servlet.{MultipartConfig, FileUploadSupport}
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
@@ -18,10 +19,9 @@ class FileUploadController extends ScalatraServlet
post("/image"){ post("/image"){
fileParams.get("file") match { fileParams.get("file") match {
case Some(file) if(FileUtil.isImage(file.name)) => { case Some(file) if(FileUtil.isImage(file.name)) => defining(generateFileId){ fileId =>
val fileId = generateFileId
FileUtils.writeByteArrayToFile(getTemporaryFile(fileId), file.get) FileUtils.writeByteArrayToFile(getTemporaryFile(fileId), file.get)
session += "upload_" + fileId -> file.name session += Keys.Session.Upload(fileId) -> file.name
Ok(fileId) Ok(fileId)
} }
case None => BadRequest case None => BadRequest

View File

@@ -4,7 +4,9 @@ import jp.sf.amateras.scalatra.forms._
import service._ import service._
import IssuesService._ import IssuesService._
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator, Notifier} import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator, Notifier, Keys}
import util.Implicits._
import util.ControlUtil._
import org.scalatra.Ok import org.scalatra.Ok
class IssuesController extends IssuesControllerBase class IssuesController extends IssuesControllerBase
@@ -57,12 +59,9 @@ trait IssuesControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/issues/:id")(referrersOnly { repository => get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
val owner = repository.owner defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) =>
val name = repository.name getIssue(owner, name, issueId) map {
val issueId = params("id") issues.html.issue(
getIssue(owner, name, issueId) map {
issues.html.issue(
_, _,
getComments(owner, name, issueId.toInt), getComments(owner, name, issueId.toInt),
getIssueLabels(owner, name, issueId.toInt), getIssueLabels(owner, name, issueId.toInt),
@@ -71,65 +70,64 @@ trait IssuesControllerBase extends ControllerBase {
getLabels(owner, name), getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount), hasWritePermission(owner, name, context.loginAccount),
repository) repository)
} getOrElse NotFound } getOrElse NotFound
}
}) })
get("/:owner/:repository/issues/new")(readableUsersOnly { repository => get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
val owner = repository.owner defining(repository.owner, repository.name){ case (owner, name) =>
val name = repository.name issues.html.create(
(getCollaborators(owner, name) :+ owner).sorted,
issues.html.create( getMilestones(owner, name),
(getCollaborators(owner, name) :+ owner).sorted, getLabels(owner, name),
getMilestones(owner, name), hasWritePermission(owner, name, context.loginAccount),
getLabels(owner, name), repository)
hasWritePermission(owner, name, context.loginAccount), }
repository)
}) })
post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
val owner = repository.owner defining(repository.owner, repository.name){ case (owner, name) =>
val name = repository.name val writable = hasWritePermission(owner, name, context.loginAccount)
val writable = hasWritePermission(owner, name, context.loginAccount) val userName = context.loginAccount.get.userName
val userName = context.loginAccount.get.userName
// insert issue // insert issue
val issueId = createIssue(owner, name, userName, form.title, form.content, val issueId = createIssue(owner, name, userName, form.title, form.content,
if(writable) form.assignedUserName else None, if(writable) form.assignedUserName else None,
if(writable) form.milestoneId else None) if(writable) form.milestoneId else None)
// insert labels // insert labels
if(writable){ if(writable){
form.labelNames.map { value => form.labelNames.map { value =>
val labels = getLabels(owner, name) val labels = getLabels(owner, name)
value.split(",").foreach { labelName => value.split(",").foreach { labelName =>
labels.find(_.labelName == labelName).map { label => labels.find(_.labelName == labelName).map { label =>
registerIssueLabel(owner, name, issueId, label.labelId) registerIssueLabel(owner, name, issueId, label.labelId)
}
} }
} }
} }
// record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
// notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgIssue(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
}
redirect(s"/${owner}/${name}/issues/${issueId}")
} }
// record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
// notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgIssue(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
}
redirect(s"/${owner}/${name}/issues/${issueId}")
}) })
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) => ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
val owner = repository.owner defining(repository.owner, repository.name){ case (owner, name) =>
val name = repository.name getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){
getIssue(owner, name, params("id")).map { issue => updateIssue(owner, name, issue.issueId, form.title, form.content)
if(isEditable(owner, name, issue.openedUserName)){ redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
updateIssue(owner, name, issue.issueId, form.title, form.content) } else Unauthorized
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") } getOrElse NotFound
} else Unauthorized }
} getOrElse NotFound
}) })
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
@@ -147,15 +145,14 @@ trait IssuesControllerBase extends ControllerBase {
}) })
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
val owner = repository.owner defining(repository.owner, repository.name){ case (owner, name) =>
val name = repository.name getComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){
getComment(owner, name, params("id")).map { comment => updateComment(comment.commentId, form.content)
if(isEditable(owner, name, comment.commentedUserName)){ redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
updateComment(comment.commentId, form.content) } else Unauthorized
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}") } getOrElse NotFound
} else Unauthorized }
} getOrElse NotFound
}) })
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository => ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
@@ -194,17 +191,17 @@ trait IssuesControllerBase extends ControllerBase {
}) })
ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
val issueId = params("id").toInt defining(params("id").toInt){ issueId =>
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) }
}) })
ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository =>
val issueId = params("id").toInt defining(params("id").toInt){ issueId =>
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) }
}) })
ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
@@ -223,36 +220,36 @@ trait IssuesControllerBase extends ControllerBase {
}) })
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
val action = params.get("value") defining(params.get("value")){ action =>
executeBatch(repository) {
executeBatch(repository) { handleComment(_, None, repository)( _ => action)
handleComment(_, None, repository)( _ => action) }
} }
}) })
post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository => post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
val labelId = params("value").toInt defining(params("value").toInt){ labelId =>
executeBatch(repository) { issueId =>
executeBatch(repository) { issueId => getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse { registerIssueLabel(repository.owner, repository.name, issueId, labelId)
registerIssueLabel(repository.owner, repository.name, issueId, labelId) }
} }
} }
}) })
post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository => post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
val value = assignedUserName("value") defining(assignedUserName("value")){ value =>
executeBatch(repository) {
executeBatch(repository) { updateAssignedUserName(repository.owner, repository.name, _, value)
updateAssignedUserName(repository.owner, repository.name, _, value) }
} }
}) })
post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository => post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository =>
val value = milestoneId("value") defining(milestoneId("value")){ value =>
executeBatch(repository) {
executeBatch(repository) { updateMilestoneId(repository.owner, repository.name, _, value)
updateMilestoneId(repository.owner, repository.name, _, value) }
} }
}) })
@@ -273,89 +270,89 @@ trait IssuesControllerBase extends ControllerBase {
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo) private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
(getAction: model.Issue => Option[String] = (getAction: model.Issue => Option[String] =
p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = { p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
val owner = repository.owner
val name = repository.name
val userName = context.loginAccount.get.userName
getIssue(owner, name, issueId.toString) map { issue => defining(repository.owner, repository.name){ case (owner, name) =>
val (action, recordActivity) = val userName = context.loginAccount.get.userName
getAction(issue)
.collect { getIssue(owner, name, issueId.toString) map { issue =>
val (action, recordActivity) =
getAction(issue)
.collect {
case "close" => true -> (Some("close") -> case "close" => true -> (Some("close") ->
Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _)) Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
case "reopen" => false -> (Some("reopen") -> case "reopen" => false -> (Some("reopen") ->
Some(recordReopenIssueActivity _)) Some(recordReopenIssueActivity _))
} }
.map { case (closed, t) => .map { case (closed, t) =>
updateClosed(owner, name, issueId, closed) updateClosed(owner, name, issueId, closed)
t t
} }
.getOrElse(None -> None) .getOrElse(None -> None)
val commentId = content val commentId = content
.map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") ) .map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") )
.getOrElse ( action.get.capitalize -> action.get ) .getOrElse ( action.get.capitalize -> action.get )
match { match {
case (content, action) => createComment(owner, name, userName, issueId, content, action) case (content, action) => createComment(owner, name, userName, issueId, content, action)
} }
// record activity // record activity
content foreach { content foreach {
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _) (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
(owner, name, userName, issueId, _) (owner, name, userName, issueId, _)
} }
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) ) recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
// notifications // notifications
Notifier() match { Notifier() match {
case f => case f =>
content foreach { content foreach {
f.toNotify(repository, issueId, _){ f.toNotify(repository, issueId, _){
Notifier.msgComment(s"${baseUrl}/${owner}/${name}/${ Notifier.msgComment(s"${baseUrl}/${owner}/${name}/${
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}") if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}")
}
} }
} action foreach {
action foreach { f.toNotify(repository, issueId, _){
f.toNotify(repository, issueId, _){ Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/issues/${issueId}") }
} }
} }
}
issue -> commentId issue -> commentId
}
} }
} }
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = { private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
val owner = repository.owner defining(repository.owner, repository.name){ case (owner, repoName) =>
val repoName = repository.name val filterUser = Map(filter -> params.getOrElse("userName", ""))
val filterUser = Map(filter -> params.getOrElse("userName", "")) val page = IssueSearchCondition.page(request)
val page = IssueSearchCondition.page(request) val sessionKey = Keys.Session.Issues(owner, repoName)
val sessionKey = s"${owner}/${repoName}/issues"
// retrieve search condition // retrieve search condition
val condition = if(request.getQueryString == null){ val condition = session.putAndGet(sessionKey,
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition] if(request.hasQueryString) IssueSearchCondition(request)
} else IssueSearchCondition(request) else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
session.put(sessionKey, condition) issues.html.list(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
issues.html.list( page,
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), (getCollaborators(owner, repoName) :+ owner).sorted,
page, getMilestones(owner, repoName),
(getCollaborators(owner, repoName) :+ owner).sorted, getLabels(owner, repoName),
getMilestones(owner, repoName), countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName),
getLabels(owner, repoName), countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName),
countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName), countIssue(condition, Map.empty, false, owner -> repoName),
countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName), context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)),
countIssue(condition, Map.empty, false, owner -> repoName), context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)),
context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)), countIssueGroupByLabels(owner, repoName, condition, filterUser),
context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)), condition,
countIssueGroupByLabels(owner, repoName, condition, filterUser), filter,
condition, repository,
filter, hasWritePermission(owner, repoName, context.loginAccount))
repository, }
hasWritePermission(owner, repoName, context.loginAccount))
} }
} }

View File

@@ -1,8 +1,9 @@
package app package app
import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier} import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier, Keys}
import util.Directory._ import util.Directory._
import util.Implicits._ import util.Implicits._
import util.ControlUtil._
import service._ import service._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
@@ -62,117 +63,250 @@ trait PullRequestsControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/pull/:id")(referrersOnly { repository => get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
val owner = repository.owner defining(repository.owner, repository.name, params("id").toInt){ case (owner, name, issueId) =>
val name = repository.name getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
val issueId = params("id").toInt using(Git.open(getRepositoryDir(owner, name))){ git =>
val (commits, diffs) =
getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
getPullRequest(owner, name, issueId) map { case(issue, pullreq) => pulls.html.pullreq(
JGitUtil.withGit(getRepositoryDir(owner, name)){ git => issue, pullreq,
val requestCommitId = git.getRepository.resolve(pullreq.requestBranch) getComments(owner, name, issueId.toInt),
(getCollaborators(owner, name) :+ owner).sorted,
val (commits, diffs) = getMilestonesWithIssueCount(owner, name),
getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo) commits,
diffs,
pulls.html.pullreq( hasWritePermission(owner, name, context.loginAccount),
issue, pullreq, repository)
getComments(owner, name, issueId.toInt),
(getCollaborators(owner, name) :+ owner).sorted,
getMilestonesWithIssueCount(owner, name),
commits,
diffs,
if(issue.closed){
false
} else {
checkConflict(owner, name, pullreq.branch, owner, name, pullreq.requestBranch)
},
hasWritePermission(owner, name, context.loginAccount),
repository,
s"${baseUrl}${context.path}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
}
} getOrElse NotFound
})
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
LockUtil.lock(s"${repository.owner}/${repository.name}/merge"){
val issueId = params("id").toInt
getPullRequest(repository.owner, repository.name, issueId).map { case (issue, pullreq) =>
val remote = getRepositoryDir(repository.owner, repository.name)
val tmpdir = new java.io.File(getTemporaryDir(repository.owner, repository.name), s"merge-${issueId}")
val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(pullreq.branch).call
try {
// mark issue as merged and close.
val loginAccount = context.loginAccount.get
createComment(repository.owner, repository.name, loginAccount.userName, issueId, form.message, "merge")
createComment(repository.owner, repository.name, loginAccount.userName, issueId, "Close", "close")
updateClosed(repository.owner, repository.name, issueId, true)
// record activity
recordMergeActivity(repository.owner, repository.name, loginAccount.userName, issueId, form.message)
// TODO apply ref comment
// fetch pull request to temporary working repository
val pullRequestBranchName = s"gitbucket-pullrequest-${issueId}"
git.fetch
.setRemote(getRepositoryDir(repository.owner, repository.name).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/pull/${issueId}/head:refs/heads/${pullRequestBranchName}")).call
// merge pull request
git.checkout.setName(pullreq.branch).call
val result = git.merge
.include(git.getRepository.resolve(pullRequestBranchName))
.setFastForward(FastForwardMode.NO_FF)
.setCommit(false)
.call
if(result.getConflicts != null){
throw new RuntimeException("This pull request can't merge automatically.")
}
// merge commit
git.getRepository.writeMergeCommitMsg(
s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestRepositoryName}\n"
+ form.message)
git.commit
.setCommitter(new PersonIdent(loginAccount.userName, loginAccount.mailAddress))
.call
// push
git.push.call
val (commits, _) = getRequestCompareInfo(repository.owner, repository.name, pullreq.commitIdFrom,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
commits.flatten.foreach { commit =>
if(!existsCommitId(repository.owner, repository.name, commit.id)){
insertCommitId(repository.owner, repository.name, commit.id)
}
}
// notifications
Notifier().toNotify(repository, issueId, "merge"){
Notifier.msgStatus(s"${baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
}
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} finally {
git.getRepository.close
FileUtils.deleteDirectory(tmpdir)
} }
} getOrElse NotFound } getOrElse NotFound
} }
}) })
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository =>
defining(repository.owner, repository.name, params("id").toInt){ case (owner, name, issueId) =>
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
pulls.html.mergeguide(
checkConflict(owner, name, pullreq.branch, owner, name, pullreq.requestBranch),
pullreq,
s"${baseUrl}${context.path}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
} getOrElse NotFound()
}
})
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
defining(repository.owner, repository.name, params("id").toInt){ case (owner, name, issueId) =>
LockUtil.lock(s"${owner}/${name}/merge"){
getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
val remote = getRepositoryDir(owner, name)
val tmpdir = new java.io.File(getTemporaryDir(owner, name), s"merge-${issueId}")
val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(pullreq.branch).call
try {
// mark issue as merged and close.
val loginAccount = context.loginAccount.get
createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
createComment(owner, name, loginAccount.userName, issueId, "Close", "close")
updateClosed(owner, name, issueId, true)
// record activity
recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
// fetch pull request to temporary working repository
val pullRequestBranchName = s"gitbucket-pullrequest-${issueId}"
git.fetch
.setRemote(getRepositoryDir(owner, name).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/pull/${issueId}/head:refs/heads/${pullRequestBranchName}")).call
// merge pull request
git.checkout.setName(pullreq.branch).call
val result = git.merge
.include(git.getRepository.resolve(pullRequestBranchName))
.setFastForward(FastForwardMode.NO_FF)
.setCommit(false)
.call
if(result.getConflicts != null){
throw new RuntimeException("This pull request can't merge automatically.")
}
// merge commit
git.getRepository.writeMergeCommitMsg(
s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestRepositoryName}\n"
+ form.message)
git.commit
.setCommitter(new PersonIdent(loginAccount.userName, loginAccount.mailAddress))
.call
// push
git.push.call
val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
commits.flatten.foreach { commit =>
if(!existsCommitId(owner, name, commit.id)){
insertCommitId(owner, name, commit.id)
}
}
// notifications
Notifier().toNotify(repository, issueId, "merge"){
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/pull/${issueId}")
}
redirect(s"/${owner}/${name}/pull/${issueId}")
} finally {
git.getRepository.close
FileUtils.deleteDirectory(tmpdir)
}
} getOrElse NotFound
}
}
})
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(originUserName), Some(originRepositoryName)) => {
getRepository(originUserName, originRepositoryName, baseUrl).map { originRepository =>
using(
Git.open(getRepositoryDir(originUserName, originRepositoryName)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
){ (oldGit, newGit) =>
val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2
val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
}
} getOrElse NotFound
}
case _ => {
using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
val defaultBranch = JGitUtil.getDefaultBranch(git, forkedRepository).get._2
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
}
}
}
})
get("/:owner/:repository/compare/*...*")(referrersOnly { repository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner)
(getRepository(originOwner, repository.name, baseUrl),
getRepository(forkedOwner, repository.name, baseUrl)) match {
case (Some(originRepository), Some(forkedRepository)) => {
using(
Git.open(getRepositoryDir(originOwner, repository.name)),
Git.open(getRepositoryDir(forkedOwner, repository.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,
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))
}
}
case _ => NotFound
}
})
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { repository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner)
(getRepository(originOwner, repository.name, baseUrl),
getRepository(forkedOwner, repository.name, baseUrl)) match {
case (Some(originRepository), Some(forkedRepository)) => {
using(
Git.open(getRepositoryDir(originOwner, repository.name)),
Git.open(getRepositoryDir(forkedOwner, repository.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(originOwner, repository.name, originBranch, forkedOwner, repository.name, forkedBranch))
}
}
case _ => NotFound()
}
})
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
val loginUserName = context.loginAccount.get.userName
val issueId = createIssue(
owner = repository.owner,
repository = repository.name,
loginUser = loginUserName,
title = form.title,
content = form.content,
assignedUserName = None,
milestoneId = None,
isPullRequest = true)
createPullRequest(
originUserName = repository.owner,
originRepositoryName = repository.name,
issueId = issueId,
originBranch = form.targetBranch,
requestUserName = form.requestUserName,
requestRepositoryName = repository.name,
requestBranch = form.requestBranch,
commitIdFrom = form.commitIdFrom,
commitIdTo = form.commitIdTo)
// fetch requested branch
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.fetch
.setRemote(getRepositoryDir(form.requestUserName, repository.name).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head"))
.call
}
// record activity
recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title)
// notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgPullRequest(s"${baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
}
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
})
/** /**
* Checks whether conflict will be caused in merging. * Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
* Returns true if conflict will be caused.
*/ */
private def checkConflict(userName: String, repositoryName: String, branch: String, private def checkConflict(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = { requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
@@ -205,134 +339,6 @@ trait PullRequestsControllerBase extends ControllerBase {
} }
} }
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(originUserName), Some(originRepositoryName)) => {
getRepository(originUserName, originRepositoryName, baseUrl).map { originRepository =>
withGit(
getRepositoryDir(originUserName, originRepositoryName),
getRepositoryDir(forkedRepository.owner, forkedRepository.name)
){ (oldGit, newGit) =>
val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2
val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
}
} getOrElse NotFound
}
case _ => {
JGitUtil.withGit(getRepositoryDir(forkedRepository.owner, forkedRepository.name)){ git =>
val defaultBranch = JGitUtil.getDefaultBranch(git, forkedRepository).get._2
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
}
}
}
})
get("/:owner/:repository/compare/*...*")(referrersOnly { repository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner)
(getRepository(originOwner, repository.name, baseUrl),
getRepository(forkedOwner, repository.name, baseUrl)) match {
case (Some(originRepository), Some(forkedRepository)) => {
withGit(
getRepositoryDir(originOwner, repository.name),
getRepositoryDir(forkedOwner, repository.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,
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,
checkConflict(originOwner, repository.name, originBranch, forkedOwner, repository.name, forkedBranch),
repository,
originRepository,
forkedRepository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
}
}
case _ => NotFound
}
})
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
val loginUserName = context.loginAccount.get.userName
val issueId = createIssue(
owner = repository.owner,
repository = repository.name,
loginUser = loginUserName,
title = form.title,
content = form.content,
assignedUserName = None,
milestoneId = None,
isPullRequest = true)
createPullRequest(
originUserName = repository.owner,
originRepositoryName = repository.name,
issueId = issueId,
originBranch = form.targetBranch,
requestUserName = form.requestUserName,
requestRepositoryName = repository.name,
requestBranch = form.requestBranch,
commitIdFrom = form.commitIdFrom,
commitIdTo = form.commitIdTo)
// fetch requested branch
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
git.fetch
.setRemote(getRepositoryDir(form.requestUserName, repository.name).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head"))
.call
}
// record activity
recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title)
// notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgPullRequest(s"${baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
}
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
})
/**
* Handles w Git object simultaneously.
*/
private def withGit[T](oldDir: java.io.File, newDir: java.io.File)(action: (Git, Git) => T): T = {
val oldGit = Git.open(oldDir)
val newGit = Git.open(newDir)
try {
action(oldGit, newGit)
} finally {
oldGit.getRepository.close
newGit.getRepository.close
}
}
/** /**
* Parses branch identifier and extracts owner and branch name as tuple. * Parses branch identifier and extracts owner and branch name as tuple.
* *
@@ -366,9 +372,9 @@ trait PullRequestsControllerBase extends ControllerBase {
private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String, private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = { requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = {
withGit( using(
getRepositoryDir(userName, repositoryName), Git.open(getRepositoryDir(userName, repositoryName)),
getRepositoryDir(requestUserName, requestRepositoryName) Git.open(getRepositoryDir(requestUserName, requestRepositoryName))
){ (oldGit, newGit) => ){ (oldGit, newGit) =>
val oldId = oldGit.getRepository.resolve(branch) val oldId = oldGit.getRepository.resolve(branch)
val newId = newGit.getRepository.resolve(requestCommitId) val newId = newGit.getRepository.resolve(requestCommitId)
@@ -385,31 +391,29 @@ trait PullRequestsControllerBase extends ControllerBase {
} }
} }
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) = { private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
val owner = repository.owner defining(repository.owner, repository.name){ case (owner, repoName) =>
val repoName = repository.name val filterUser = userName.map { x => Map("created_by" -> x) } getOrElse Map("all" -> "")
val filterUser = userName.map { x => Map("created_by" -> x) } getOrElse Map("all" -> "") val page = IssueSearchCondition.page(request)
val page = IssueSearchCondition.page(request) val sessionKey = Keys.Session.Pulls(owner, repoName)
val sessionKey = s"${owner}/${repoName}/pulls"
// retrieve search condition // retrieve search condition
val condition = if(request.getQueryString == null){ val condition = session.putAndGet(sessionKey,
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition] if(request.hasQueryString) IssueSearchCondition(request)
} else IssueSearchCondition(request) else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
session.put(sessionKey, condition) pulls.html.list(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
pulls.html.list( getPullRequestCountGroupByUser(condition.state == "closed", owner, Some(repoName)),
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), userName,
getPullRequestCountGroupByUser(condition.state == "closed", owner, Some(repoName)), page,
userName, countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName),
page, countIssue(condition.copy(state = "closed"), filterUser, true, owner -> repoName),
countIssue(condition.copy(state = "open"), filterUser, true, owner -> repoName), countIssue(condition, Map.empty, true, owner -> repoName),
countIssue(condition.copy(state = "closed"), filterUser, true, owner -> repoName), condition,
countIssue(condition, Map.empty, true, owner -> repoName), repository,
condition, hasWritePermission(owner, repoName, context.loginAccount))
repository, }
hasWritePermission(owner, repoName, context.loginAccount))
}
} }

View File

@@ -6,13 +6,20 @@ import util.{UsersAuthenticator, OwnerAuthenticator}
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.scalatra.FlashMapSupport import org.scalatra.FlashMapSupport
import service.WebHookService.WebHookPayload
import util.JGitUtil.CommitInfo
import util.ControlUtil._
import org.eclipse.jgit.api.Git
class RepositorySettingsController extends RepositorySettingsControllerBase class RepositorySettingsController extends RepositorySettingsControllerBase
with RepositoryService with AccountService with OwnerAuthenticator with UsersAuthenticator with RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator
trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSupport { trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSupport {
self: RepositoryService with AccountService with OwnerAuthenticator with UsersAuthenticator => self: RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator =>
// for repository options
case class OptionsForm(description: Option[String], defaultBranch: String, isPrivate: Boolean) case class OptionsForm(description: Option[String], defaultBranch: String, isPrivate: Boolean)
val optionsForm = mapping( val optionsForm = mapping(
@@ -20,13 +27,21 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))), "defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
"isPrivate" -> trim(label("Repository Type", boolean())) "isPrivate" -> trim(label("Repository Type", boolean()))
)(OptionsForm.apply) )(OptionsForm.apply)
// for collaborator addition
case class CollaboratorForm(userName: String) case class CollaboratorForm(userName: String)
val collaboratorForm = mapping( val collaboratorForm = mapping(
"userName" -> trim(label("Username", text(required, collaborator))) "userName" -> trim(label("Username", text(required, collaborator)))
)(CollaboratorForm.apply) )(CollaboratorForm.apply)
// for web hook url addition
case class WebHookForm(url: String)
val webHookForm = mapping(
"url" -> trim(label("url", text(required, webHook)))
)(WebHookForm.apply)
/** /**
* Redirect to the Options page. * Redirect to the Options page.
*/ */
@@ -88,6 +103,53 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
}) })
/**
* Display the web hook page.
*/
get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
settings.html.hooks(getWebHookURLs(repository.owner, repository.name), repository, flash.get("info"))
})
/**
* Add the web hook URL.
*/
post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) =>
addWebHookURL(repository.owner, repository.name, form.url)
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
})
/**
* Delete the web hook URL.
*/
get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository =>
deleteWebHookURL(repository.owner, repository.name, params("url"))
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
})
/**
* Send the test request to registered web hook URLs.
*/
get("/:owner/:repository/settings/hooks/test")(ownerOnly { repository =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
import scala.collection.JavaConverters._
val commits = git.log
.add(git.getRepository.resolve(repository.repository.defaultBranch))
.setMaxCount(3)
.call.iterator.asScala.map(new CommitInfo(_))
callWebHook(repository.owner, repository.name,
WebHookPayload(
git,
"refs/heads/" + repository.repository.defaultBranch,
repository,
commits.toList,
getAccountByUserName(repository.owner).get))
flash += "info" -> "Test payload deployed!"
}
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
})
/** /**
* Display the delete repository page. * Display the delete repository page.
*/ */
@@ -108,19 +170,25 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
redirect(s"/${repository.owner}") redirect(s"/${repository.owner}")
}) })
/**
* Provides duplication check for web hook url.
*/
private def webHook: Constraint = new Constraint(){
override def validate(name: String, value: String): Option[String] =
getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.")
}
/** /**
* Provides Constraint to validate the collaborator name. * Provides Constraint to validate the collaborator name.
*/ */
private def collaborator: Constraint = new Constraint(){ private def collaborator: Constraint = new Constraint(){
override def validate(name: String, value: String): Option[String] = { override def validate(name: String, value: String): Option[String] =
val paths = request.getRequestURI.split("/")
getAccountByUserName(value) match { getAccountByUserName(value) match {
case None => Some("User does not exist.") case None => Some("User does not exist.")
case Some(x) if(x.userName == paths(1) || getCollaborators(paths(1), paths(2)).contains(x.userName)) case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName))
=> Some("User can access this repository already.") => Some("User can access this repository already.")
case _ => None case _ => None
} }
}
} }
} }

View File

@@ -2,6 +2,7 @@ package app
import util.Directory._ import util.Directory._
import util.Implicits._ import util.Implicits._
import util.ControlUtil._
import _root_.util.{ReferrerAuthenticator, JGitUtil, FileUtil, StringUtil} import _root_.util.{ReferrerAuthenticator, JGitUtil, FileUtil, StringUtil}
import service._ import service._
import org.scalatra._ import org.scalatra._
@@ -56,7 +57,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val (branchName, path) = splitPath(repository, multiParams("splat").head) val (branchName, path) = splitPath(repository, multiParams("splat").head)
val page = params.getOrElse("page", "1").toInt val page = params.getOrElse("page", "1").toInt
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, branchName, page, 30, path) match { JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
case Right((logs, hasNext)) => case Right((logs, hasNext)) =>
repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository, repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
@@ -75,7 +76,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val (id, path) = splitPath(repository, multiParams("splat").head) val (id, path) = splitPath(repository, multiParams("splat").head)
val raw = params.get("raw").getOrElse("false").toBoolean val raw = params.get("raw").getOrElse("false").toBoolean
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id)) val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
@scala.annotation.tailrec @scala.annotation.tailrec
@@ -84,19 +85,25 @@ trait RepositoryViewerControllerBase extends ControllerBase {
case true => getPathObjectId(path, walk) case true => getPathObjectId(path, walk)
} }
val treeWalk = new TreeWalk(git.getRepository) val objectId = using(new TreeWalk(git.getRepository)){ treeWalk =>
val objectId = try {
treeWalk.addTree(revCommit.getTree) treeWalk.addTree(revCommit.getTree)
treeWalk.setRecursive(true) treeWalk.setRecursive(true)
getPathObjectId(path, treeWalk) getPathObjectId(path, treeWalk)
} finally {
treeWalk.release
} }
if(raw){ if(raw){
// Download // Download
contentType = "application/octet-stream" val mimeType = FileUtil.getMimeType(path)
JGitUtil.getContent(git, objectId, false).get val bytes = JGitUtil.getContent(git, objectId, false).get
contentType = if(mimeType == "application/octet-stream" && FileUtil.isText(bytes)){
"text/plain"
} else {
mimeType
}
//response.setHeader("Content-Disposition", s"inline; filename=${FileUtil.getFileName(path)}")
bytes
} else { } else {
// Viewer // Viewer
val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize) val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize)
@@ -127,14 +134,14 @@ trait RepositoryViewerControllerBase extends ControllerBase {
get("/:owner/:repository/commit/:id")(referrersOnly { repository => get("/:owner/:repository/commit/:id")(referrersOnly { repository =>
val id = params("id") val id = params("id")
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id)) defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))){ revCommit =>
JGitUtil.getDiffs(git, id) match { case (diffs, oldCommitId) =>
JGitUtil.getDiffs(git, id) match { case (diffs, oldCommitId) => repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
repo.html.commit(id, new JGitUtil.CommitInfo(revCommit), JGitUtil.getBranchesOfCommit(git, revCommit.getName),
JGitUtil.getBranchesOfCommit(git, revCommit.getName), JGitUtil.getTagsOfCommit(git, revCommit.getName),
JGitUtil.getTagsOfCommit(git, revCommit.getName), repository, diffs, oldCommitId)
repository, diffs, oldCommitId) }
} }
} }
}) })
@@ -143,7 +150,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
* Displays branches. * Displays branches.
*/ */
get("/:owner/:repository/branches")(referrersOnly { repository => get("/:owner/:repository/branches")(referrersOnly { repository =>
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
// retrieve latest update date of each branch // retrieve latest update date of each branch
val branchInfo = repository.branchList.map { branchName => val branchInfo = repository.branchList.map { branchName =>
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
@@ -176,7 +183,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
// clone the repository // clone the repository
val cloneDir = new File(workDir, revision) val cloneDir = new File(workDir, revision)
JGitUtil.withGit(Git.cloneRepository using(Git.cloneRepository
.setURI(getRepositoryDir(repository.owner, repository.name).toURI.toString) .setURI(getRepositoryDir(repository.owner, repository.name).toURI.toString)
.setDirectory(cloneDir) .setDirectory(cloneDir)
.call){ git => .call){ git =>
@@ -233,23 +240,23 @@ trait RepositoryViewerControllerBase extends ControllerBase {
if(repository.commitCount == 0){ if(repository.commitCount == 0){
repo.html.guide(repository) repo.html.guide(repository)
} else { } else {
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head) val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head)
// get specified commit // get specified commit
JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) => JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
val revCommit = JGitUtil.getRevCommitFromId(git, objectId) defining(JGitUtil.getRevCommitFromId(git, objectId)){ revCommit =>
// get files // get files
val files = JGitUtil.getFileList(git, revision, path) val files = JGitUtil.getFileList(git, revision, path)
// process README.md // process README.md
val readme = files.find(_.name == "README.md").map { file => val readme = files.find(_.name == "README.md").map { file =>
StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get) StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
} }
repo.html.files(revision, repository, repo.html.files(revision, repository,
if(path == ".") Nil else path.split("/").toList, // current path if(path == ".") Nil else path.split("/").toList, // current path
new JGitUtil.CommitInfo(revCommit), // latest commit new JGitUtil.CommitInfo(revCommit), // latest commit
files, readme) files, readme)
}
} getOrElse NotFound } getOrElse NotFound
} }
} }

View File

@@ -1,6 +1,7 @@
package app package app
import util._ import util._
import ControlUtil._
import service._ import service._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
@@ -26,25 +27,25 @@ trait SearchControllerBase extends ControllerBase { self: RepositoryService
} }
get("/:owner/:repository/search")(referrersOnly { repository => get("/:owner/:repository/search")(referrersOnly { repository =>
val query = params("q").trim defining(params("q").trim, params.getOrElse("type", "code")){ case (query, target) =>
val target = params.getOrElse("type", "code") val page = try {
val page = try { val i = params.getOrElse("page", "1").toInt
val i = params.getOrElse("page", "1").toInt if(i <= 0) 1 else i
if(i <= 0) 1 else i } catch {
} catch { case e: NumberFormatException => 1
case e: NumberFormatException => 1 }
}
target.toLowerCase match { target.toLowerCase match {
case "issue" => search.html.issues( case "issue" => search.html.issues(
searchIssues(repository.owner, repository.name, query), searchIssues(repository.owner, repository.name, query),
countFiles(repository.owner, repository.name, query), countFiles(repository.owner, repository.name, query),
query, page, repository) query, page, repository)
case _ => search.html.code( case _ => search.html.code(
searchFiles(repository.owner, repository.name, query), searchFiles(repository.owner, repository.name, query),
countIssues(repository.owner, repository.name, query), countIssues(repository.owner, repository.name, query),
query, page, repository) query, page, repository)
}
} }
}) })

View File

@@ -2,6 +2,8 @@ package app
import service._ import service._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import util.Implicits._
import util.Keys
class SignInController extends SignInControllerBase with SystemSettingsService with AccountService class SignInController extends SignInControllerBase with SystemSettingsService with AccountService
@@ -17,16 +19,15 @@ trait SignInControllerBase extends ControllerBase { self: SystemSettingsService
get("/signin"){ get("/signin"){
val redirect = params.get("redirect") val redirect = params.get("redirect")
if(redirect.isDefined && redirect.get.startsWith("/")){ if(redirect.isDefined && redirect.get.startsWith("/")){
session.setAttribute("REDIRECT", redirect.get) session.setAttribute(Keys.Session.Redirect, redirect.get)
} }
html.signin(loadSystemSettings()) html.signin(loadSystemSettings())
} }
post("/signin", form){ form => post("/signin", form){ form =>
val settings = loadSystemSettings()
authenticate(loadSystemSettings(), form.userName, form.password) match { authenticate(loadSystemSettings(), form.userName, form.password) match {
case Some(account) => signin(account) case Some(account) => signin(account)
case None => redirect("/signin") case None => redirect("/signin")
} }
} }
@@ -39,12 +40,15 @@ trait SignInControllerBase extends ControllerBase { self: SystemSettingsService
* Set account information into HttpSession and redirect. * Set account information into HttpSession and redirect.
*/ */
private def signin(account: model.Account) = { private def signin(account: model.Account) = {
session.setAttribute("LOGIN_ACCOUNT", account) session.setAttribute(Keys.Session.LoginAccount, account)
updateLastLoginDate(account.userName) updateLastLoginDate(account.userName)
session.get("REDIRECT").map { redirectUrl => session.getAndRemove[String](Keys.Session.Redirect).map { redirectUrl =>
session.removeAttribute("REDIRECT") if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){
redirect(redirectUrl.asInstanceOf[String]) redirect("/")
} else {
redirect(redirectUrl)
}
}.getOrElse { }.getOrElse {
redirect("/") redirect("/")
} }

View File

@@ -3,6 +3,7 @@ package app
import service._ import service._
import util.AdminAuthenticator import util.AdminAuthenticator
import util.StringUtil._ import util.StringUtil._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
class UserManagementController extends UserManagementControllerBase class UserManagementController extends UserManagementControllerBase
@@ -107,29 +108,29 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
}) })
get("/admin/users/:groupName/_editgroup")(adminOnly { get("/admin/users/:groupName/_editgroup")(adminOnly {
val groupName = params("groupName") defining(params("groupName")){ groupName =>
admin.users.html.group(getAccountByUserName(groupName), getGroupMembers(groupName)) admin.users.html.group(getAccountByUserName(groupName), getGroupMembers(groupName))
}
}) })
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form => post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
val groupName = params("groupName") defining(params("groupName"), form.memberNames.map(_.split(",").toList).getOrElse(Nil)){ case (groupName, memberNames) =>
getAccountByUserName(groupName).map { account => getAccountByUserName(groupName).map { account =>
updateGroup(groupName, form.url) updateGroup(groupName, form.url)
updateGroupMembers(form.groupName, memberNames)
val memberNames = form.memberNames.map(_.split(",").toList).getOrElse(Nil) getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
updateGroupMembers(form.groupName, memberNames) removeCollaborators(form.groupName, repositoryName)
memberNames.foreach { userName =>
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => addCollaborator(form.groupName, repositoryName, userName)
removeCollaborators(form.groupName, repositoryName) }
memberNames.foreach { userName =>
addCollaborator(form.groupName, repositoryName, userName)
} }
}
updateImage(form.groupName, form.fileId, form.clearImage) updateImage(form.groupName, form.fileId, form.clearImage)
redirect("/admin/users") redirect("/admin/users")
} getOrElse NotFound } getOrElse NotFound
}
}) })
post("/admin/users/_usercheck")(adminOnly { post("/admin/users/_usercheck")(adminOnly {

View File

@@ -3,7 +3,9 @@ package app
import service._ import service._
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, JGitUtil, StringUtil} import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, JGitUtil, StringUtil}
import util.Directory._ import util.Directory._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.api.Git
class WikiController extends WikiControllerBase class WikiController extends WikiControllerBase
with WikiService with RepositoryService with AccountService with ActivityService with WikiService with RepositoryService with AccountService with ActivityService
@@ -40,13 +42,13 @@ trait WikiControllerBase extends ControllerBase {
getWikiPage(repository.owner, repository.name, pageName).map { page => getWikiPage(repository.owner, repository.name, pageName).map { page =>
wiki.html.page(pageName, page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount)) wiki.html.page(pageName, page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${pageName}/_edit") // TODO URLEncode } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
}) })
get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository => get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository =>
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match { JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match {
case Right((logs, hasNext)) => wiki.html.history(Some(pageName), logs, repository) case Right((logs, hasNext)) => wiki.html.history(Some(pageName), logs, repository)
case Left(_) => NotFound case Left(_) => NotFound
@@ -58,7 +60,7 @@ trait WikiControllerBase extends ControllerBase {
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
val commitId = params("commitId").split("\\.\\.\\.") val commitId = params("commitId").split("\\.\\.\\.")
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
wiki.html.compare(Some(pageName), JGitUtil.getDiffs(git, commitId(0), commitId(1), true), repository) wiki.html.compare(Some(pageName), JGitUtil.getDiffs(git, commitId(0), commitId(1), true), repository)
} }
}) })
@@ -66,7 +68,7 @@ trait WikiControllerBase extends ControllerBase {
get("/:owner/:repository/wiki/_compare/:commitId")(referrersOnly { repository => get("/:owner/:repository/wiki/_compare/:commitId")(referrersOnly { repository =>
val commitId = params("commitId").split("\\.\\.\\.") val commitId = params("commitId").split("\\.\\.\\.")
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
wiki.html.compare(None, JGitUtil.getDiffs(git, commitId(0), commitId(1), true), repository) wiki.html.compare(None, JGitUtil.getDiffs(git, commitId(0), commitId(1), true), repository)
} }
}) })
@@ -120,7 +122,7 @@ trait WikiControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/wiki/_history")(referrersOnly { repository => get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, "master") match { JGitUtil.getCommitLog(git, "master") match {
case Right((logs, hasNext)) => wiki.html.history(None, logs, repository) case Right((logs, hasNext)) => wiki.html.history(None, logs, repository)
case Left(_) => NotFound case Left(_) => NotFound
@@ -130,8 +132,8 @@ trait WikiControllerBase extends ControllerBase {
get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository => get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository =>
getFileContent(repository.owner, repository.name, multiParams("splat").head).map { content => getFileContent(repository.owner, repository.name, multiParams("splat").head).map { content =>
contentType = "application/octet-stream" contentType = "application/octet-stream"
content content
} getOrElse NotFound } getOrElse NotFound
}) })

View File

@@ -0,0 +1,16 @@
package model
import scala.slick.driver.H2Driver.simple._
object WebHooks extends Table[WebHook]("WEB_HOOK") with BasicTemplate {
def url = column[String]("URL")
def * = userName ~ repositoryName ~ url <> (WebHook, WebHook.unapply _)
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url is url.bind)
}
case class WebHook(
userName: String,
repositoryName: String,
url: String
)

View File

@@ -2,21 +2,21 @@ package service
import scala.slick.driver.H2Driver.simple._ import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession import Database.threadLocalSession
import model._ import model._
import util.ControlUtil._
trait PullRequestService { self: IssuesService => trait PullRequestService { self: IssuesService =>
import PullRequestService._ import PullRequestService._
def getPullRequest(owner: String, repository: String, issueId: Int): Option[(Issue, PullRequest)] = { def getPullRequest(owner: String, repository: String, issueId: Int): Option[(Issue, PullRequest)] =
val issue = getIssue(owner, repository, issueId.toString) defining(getIssue(owner, repository, issueId.toString)){ issue =>
if(issue.isDefined){ if(issue.isDefined){
Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).firstOption match { Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).firstOption match {
case Some(pullreq) => Some((issue.get, pullreq)) case Some(pullreq) => Some((issue.get, pullreq))
case None => None case None => None
} }
} else None } else None
} }
def getPullRequestCountGroupByUser(closed: Boolean, owner: String, repository: Option[String]): List[PullRequestCount] = def getPullRequestCountGroupByUser(closed: Boolean, owner: String, repository: Option[String]): List[PullRequestCount] =
Query(PullRequests) Query(PullRequests)

View File

@@ -2,6 +2,7 @@ package service
import util.{FileUtil, StringUtil, JGitUtil} import util.{FileUtil, StringUtil, JGitUtil}
import util.Directory._ import util.Directory._
import util.ControlUtil._
import model.Issue import model.Issue
import org.eclipse.jgit.revwalk.RevWalk import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.TreeWalk import org.eclipse.jgit.treewalk.TreeWalk
@@ -9,7 +10,8 @@ import scala.collection.mutable.ListBuffer
import org.eclipse.jgit.lib.FileMode import org.eclipse.jgit.lib.FileMode
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
trait RepositorySearchService { self: IssuesService => trait
RepositorySearchService { self: IssuesService =>
import RepositorySearchService._ import RepositorySearchService._
def countIssues(owner: String, repository: String, query: String): Int = def countIssues(owner: String, repository: String, query: String): Int =
@@ -27,12 +29,12 @@ trait RepositorySearchService { self: IssuesService =>
} }
def countFiles(owner: String, repository: String, query: String): Int = def countFiles(owner: String, repository: String, query: String): Int =
JGitUtil.withGit(getRepositoryDir(owner, repository)){ git => using(Git.open(getRepositoryDir(owner, repository))){ git =>
if(JGitUtil.isEmpty(git)) 0 else searchRepositoryFiles(git, query).length if(JGitUtil.isEmpty(git)) 0 else searchRepositoryFiles(git, query).length
} }
def searchFiles(owner: String, repository: String, query: String): List[FileSearchResult] = def searchFiles(owner: String, repository: String, query: String): List[FileSearchResult] =
JGitUtil.withGit(getRepositoryDir(owner, repository)){ git => using(Git.open(getRepositoryDir(owner, repository))){ git =>
if(JGitUtil.isEmpty(git)){ if(JGitUtil.isEmpty(git)){
Nil Nil
} else { } else {

View File

@@ -47,8 +47,10 @@ trait RepositoryService { self: AccountService =>
Labels .filter(_.byRepository(userName, repositoryName)).delete Labels .filter(_.byRepository(userName, repositoryName)).delete
IssueComments .filter(_.byRepository(userName, repositoryName)).delete IssueComments .filter(_.byRepository(userName, repositoryName)).delete
Issues .filter(_.byRepository(userName, repositoryName)).delete Issues .filter(_.byRepository(userName, repositoryName)).delete
PullRequests .filter(_.byRepository(userName, repositoryName)).delete
IssueId .filter(_.byRepository(userName, repositoryName)).delete IssueId .filter(_.byRepository(userName, repositoryName)).delete
Milestones .filter(_.byRepository(userName, repositoryName)).delete Milestones .filter(_.byRepository(userName, repositoryName)).delete
WebHooks .filter(_.byRepository(userName, repositoryName)).delete
Repositories .filter(_.byRepository(userName, repositoryName)).delete Repositories .filter(_.byRepository(userName, repositoryName)).delete
} }

View File

@@ -1,73 +1,76 @@
package service package service
import util.Directory._ import util.Directory._
import util.ControlUtil._
import SystemSettingsService._ import SystemSettingsService._
trait SystemSettingsService { trait SystemSettingsService {
def saveSystemSettings(settings: SystemSettings): Unit = { def saveSystemSettings(settings: SystemSettings): Unit = {
val props = new java.util.Properties() defining(new java.util.Properties()){ props =>
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)
if(settings.notification) { if(settings.notification) {
settings.smtp.foreach { smtp => settings.smtp.foreach { smtp =>
props.setProperty(SmtpHost, smtp.host) props.setProperty(SmtpHost, smtp.host)
smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString)) smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString))
smtp.user.foreach(props.setProperty(SmtpUser, _)) smtp.user.foreach(props.setProperty(SmtpUser, _))
smtp.password.foreach(props.setProperty(SmtpPassword, _)) smtp.password.foreach(props.setProperty(SmtpPassword, _))
smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString)) smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString))
}
} }
} props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString)
props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString) if(settings.ldapAuthentication){
if(settings.ldapAuthentication){ settings.ldap.map { ldap =>
settings.ldap.map { ldap => props.setProperty(LdapHost, ldap.host)
props.setProperty(LdapHost, ldap.host) ldap.port.foreach(x => props.setProperty(LdapPort, x.toString))
ldap.port.foreach(x => props.setProperty(LdapPort, x.toString)) ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x))
ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x)) 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) props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute)
props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute) }
} }
props.store(new java.io.FileOutputStream(GitBucketConf), null)
} }
props.store(new java.io.FileOutputStream(GitBucketConf), null)
} }
def loadSystemSettings(): SystemSettings = { def loadSystemSettings(): SystemSettings = {
val props = new java.util.Properties() defining(new java.util.Properties()){ props =>
if(GitBucketConf.exists){ if(GitBucketConf.exists){
props.load(new java.io.FileInputStream(GitBucketConf)) props.load(new java.io.FileInputStream(GitBucketConf))
}
SystemSettings(
getValue(props, AllowAccountRegistration, false),
getValue(props, Gravatar, true),
getValue(props, Notification, false),
if(getValue(props, Notification, false)){
Some(Smtp(
getValue(props, SmtpHost, ""),
getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
getOptionValue(props, SmtpUser, None),
getOptionValue(props, SmtpPassword, None),
getOptionValue[Boolean](props, SmtpSsl, None)))
} else {
None
},
getValue(props, LdapAuthentication, false),
if(getValue(props, LdapAuthentication, false)){
Some(Ldap(
getValue(props, LdapHost, ""),
getOptionValue(props, LdapPort, Some(DefaultLdapPort)),
getOptionValue(props, LdapBindDN, None),
getOptionValue(props, LdapBindPassword, None),
getValue(props, LdapBaseDN, ""),
getValue(props, LdapUserNameAttribute, ""),
getValue(props, LdapMailAddressAttribute, "")))
} else {
None
} }
) SystemSettings(
getValue(props, AllowAccountRegistration, false),
getValue(props, Gravatar, true),
getValue(props, Notification, false),
if(getValue(props, Notification, false)){
Some(Smtp(
getValue(props, SmtpHost, ""),
getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
getOptionValue(props, SmtpUser, None),
getOptionValue(props, SmtpPassword, None),
getOptionValue[Boolean](props, SmtpSsl, None)))
} else {
None
},
getValue(props, LdapAuthentication, false),
if(getValue(props, LdapAuthentication, false)){
Some(Ldap(
getValue(props, LdapHost, ""),
getOptionValue(props, LdapPort, Some(DefaultLdapPort)),
getOptionValue(props, LdapBindDN, None),
getOptionValue(props, LdapBindPassword, None),
getValue(props, LdapBaseDN, ""),
getValue(props, LdapUserNameAttribute, ""),
getValue(props, LdapMailAddressAttribute, "")))
} else {
None
}
)
}
} }
} }
@@ -119,23 +122,23 @@ object SystemSettingsService {
private val LdapUserNameAttribute = "ldap.username_attribute" private val LdapUserNameAttribute = "ldap.username_attribute"
private val LdapMailAddressAttribute = "ldap.mail_attribute" private val LdapMailAddressAttribute = "ldap.mail_attribute"
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = { private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
val value = props.getProperty(key) defining(props.getProperty(key)){ value =>
if(value == null || value.isEmpty) default if(value == null || value.isEmpty) default
else convertType(value).asInstanceOf[A] else convertType(value).asInstanceOf[A]
} }
private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] = { private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] =
val value = props.getProperty(key) defining(props.getProperty(key)){ value =>
if(value == null || value.isEmpty) default if(value == null || value.isEmpty) default
else Some(convertType(value)).asInstanceOf[Option[A]] else Some(convertType(value)).asInstanceOf[Option[A]]
} }
private def convertType[A: ClassTag](value: String) = { private def convertType[A: ClassTag](value: String) =
val c = implicitly[ClassTag[A]].runtimeClass defining(implicitly[ClassTag[A]].runtimeClass){ c =>
if(c == classOf[Boolean]) value.toBoolean if(c == classOf[Boolean]) value.toBoolean
else if(c == classOf[Int]) value.toInt else if(c == classOf[Int]) value.toInt
else value else value
} }
} }

View File

@@ -0,0 +1,145 @@
package service
import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession
import model._
import org.slf4j.LoggerFactory
import service.RepositoryService.RepositoryInfo
import util.JGitUtil
import org.eclipse.jgit.diff.DiffEntry
import util.JGitUtil.CommitInfo
import org.eclipse.jgit.api.Git
import org.apache.http.message.BasicNameValuePair
import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.protocol.HTTP
import org.apache.http.NameValuePair
trait WebHookService {
import WebHookService._
private val logger = LoggerFactory.getLogger(classOf[WebHookService])
def getWebHookURLs(owner: String, repository: String): List[WebHook] =
Query(WebHooks).filter(_.byRepository(owner, repository)).sortBy(_.url).list
def addWebHookURL(owner: String, repository: String, url :String): Unit =
WebHooks.insert(WebHook(owner, repository, url))
def deleteWebHookURL(owner: String, repository: String, url :String): Unit =
Query(WebHooks).filter(_.byPrimaryKey(owner, repository, url)).delete
def callWebHook(owner: String, repository: String, payload: WebHookPayload): Unit = {
import org.json4s._
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.{read, write}
import org.apache.http.client.methods.HttpPost
import org.apache.http.impl.client.DefaultHttpClient
import scala.concurrent._
import ExecutionContext.Implicits.global
logger.debug("start callWebHook")
implicit val formats = Serialization.formats(NoTypeHints)
val webHookURLs = getWebHookURLs(owner, repository)
if(webHookURLs.nonEmpty){
val json = write(payload)
val httpClient = new DefaultHttpClient()
webHookURLs.foreach { webHookUrl =>
val f = future {
logger.debug(s"start web hook invocation for ${webHookUrl}")
val httpPost = new HttpPost(webHookUrl.url)
val params: java.util.List[NameValuePair] = new java.util.ArrayList()
params.add(new BasicNameValuePair("payload", json))
httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8"))
httpClient.execute(httpPost)
httpPost.releaseConnection()
logger.debug(s"end web hook invocation for ${webHookUrl}")
}
f.onSuccess {
case s => logger.debug(s"Success: web hook request to ${webHookUrl.url}")
}
f.onFailure {
case t => logger.error(s"Failed: web hook request to ${webHookUrl.url}", t)
}
}
}
logger.debug("end callWebHook")
}
}
object WebHookService {
case class WebHookPayload(
ref: String,
commits: List[WebHookCommit],
repository: WebHookRepository)
object WebHookPayload {
def apply(git: Git, refName: String, repositoryInfo: RepositoryInfo,
commits: List[CommitInfo], repositoryOwner: Account): WebHookPayload =
WebHookPayload(
refName,
commits.map { commit =>
val diffs = JGitUtil.getDiffs(git, commit.id, false)
val commitUrl = repositoryInfo.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/commit/" + commit.id
WebHookCommit(
id = commit.id,
message = commit.fullMessage,
timestamp = commit.time.toString,
url = commitUrl,
added = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.ADD) => x.newPath },
removed = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.DELETE) => x.oldPath },
modified = diffs._1.collect { case x if(x.changeType != DiffEntry.ChangeType.ADD &&
x.changeType != DiffEntry.ChangeType.DELETE) => x.newPath },
author = WebHookUser(
name = commit.committer,
email = commit.mailAddress
)
)
}.toList,
WebHookRepository(
name = repositoryInfo.name,
url = repositoryInfo.url,
description = repositoryInfo.repository.description.getOrElse(""),
watchers = 0,
forks = repositoryInfo.forkedCount,
`private` = repositoryInfo.repository.isPrivate,
owner = WebHookUser(
name = repositoryOwner.userName,
email = repositoryOwner.mailAddress
)
)
)
}
case class WebHookCommit(
id: String,
message: String,
timestamp: String,
url: String,
added: List[String],
removed: List[String],
modified: List[String],
author: WebHookUser)
case class WebHookRepository(
name: String,
url: String,
description: String,
watchers: Int,
forks: Int,
`private`: Boolean,
owner: WebHookUser)
case class WebHookUser(
name: String,
email: String)
}

View File

@@ -5,6 +5,7 @@ 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.{Directory, JGitUtil, LockUtil} import util.{Directory, JGitUtil, LockUtil}
import util.ControlUtil._
object WikiService { object WikiService {
@@ -33,40 +34,40 @@ object WikiService {
trait WikiService { trait WikiService {
import WikiService._ import WikiService._
def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit = { def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit =
LockUtil.lock(s"${owner}/${repository}/wiki"){ LockUtil.lock(s"${owner}/${repository}/wiki"){
val dir = Directory.getWikiRepositoryDir(owner, repository) defining(Directory.getWikiRepositoryDir(owner, repository)){ dir =>
if(!dir.exists){ if(!dir.exists){
try { try {
JGitUtil.initRepository(dir) JGitUtil.initRepository(dir)
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit") saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit")
} finally { } finally {
// once delete cloned repository because initial cloned repository does not have 'branch.master.merge' // once delete cloned repository because initial cloned repository does not have 'branch.master.merge'
FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner, repository)) FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner, repository))
}
} }
} }
} }
}
/** /**
* Returns the wiki page. * Returns the wiki page.
*/ */
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = { def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git => using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
if(!JGitUtil.isEmpty(git)){ optionIf(!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) WikiPageInfo(file.name, new String(git.getRepository.open(file.id).getBytes, "UTF-8"), file.committer, file.time)
} }
} else None }
} }
} }
/** /**
* Returns the content of the specified file. * Returns the content of the specified file.
*/ */
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] = { def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] =
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git => using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
if(!JGitUtil.isEmpty(git)){ optionIf(!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)
@@ -74,15 +75,14 @@ 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 }
} }
}
/** /**
* Returns the list of wiki page names. * Returns the list of wiki page names.
*/ */
def getWikiPageList(owner: String, repository: String): List[String] = { def getWikiPageList(owner: String, repository: String): List[String] = {
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git => using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
JGitUtil.getFileList(git, "master", ".") JGitUtil.getFileList(git, "master", ".")
.filter(_.name.endsWith(".md")) .filter(_.name.endsWith(".md"))
.map(_.name.replaceFirst("\\.md$", "")) .map(_.name.replaceFirst("\\.md$", ""))
@@ -97,36 +97,31 @@ trait WikiService {
content: String, committer: model.Account, message: String): Option[String] = { content: String, committer: model.Account, message: String): Option[String] = {
LockUtil.lock(s"${owner}/${repository}/wiki"){ LockUtil.lock(s"${owner}/${repository}/wiki"){
// clone working copy defining(Directory.getWikiWorkDir(owner, repository)){ workDir =>
val workDir = Directory.getWikiWorkDir(owner, repository) // clone working copy
cloneOrPullWorkingCopy(workDir, owner, repository) cloneOrPullWorkingCopy(workDir, owner, repository)
// write as file // write as file
JGitUtil.withGit(workDir){ git => using(Git.open(workDir)){ git =>
val file = new File(workDir, newPageName + ".md") defining(new File(workDir, newPageName + ".md")){ file =>
val added = if(!file.exists || FileUtils.readFileToString(file, "UTF-8") != content){ val added = executeIf(!file.exists || FileUtils.readFileToString(file, "UTF-8") != content){
FileUtils.writeStringToFile(file, content, "UTF-8") FileUtils.writeStringToFile(file, content, "UTF-8")
git.add.addFilepattern(file.getName).call git.add.addFilepattern(file.getName).call
true }
} else {
false
}
// delete file // delete file
val deleted = if(currentPageName != "" && currentPageName != newPageName){ val deleted = executeIf(currentPageName != "" && currentPageName != newPageName){
git.rm.addFilepattern(currentPageName + ".md").call git.rm.addFilepattern(currentPageName + ".md").call
true }
} else {
false
}
// commit and push // commit and push
if(added || deleted){ optionIf(added || deleted){
val commit = git.commit.setCommitter(committer.userName, committer.mailAddress).setMessage(message).call defining(git.commit.setCommitter(committer.userName, committer.mailAddress).setMessage(message).call){ commit =>
git.push.call git.push.call
Some(commit.getName) Some(commit.getName)
} else { }
None }
}
} }
} }
} }
@@ -138,35 +133,34 @@ trait WikiService {
def deleteWikiPage(owner: String, repository: String, pageName: String, def deleteWikiPage(owner: String, repository: String, pageName: String,
committer: String, mailAddress: String, message: String): Unit = { committer: String, mailAddress: String, message: String): Unit = {
LockUtil.lock(s"${owner}/${repository}/wiki"){ LockUtil.lock(s"${owner}/${repository}/wiki"){
// clone working copy defining(Directory.getWikiWorkDir(owner, repository)){ workDir =>
val workDir = Directory.getWikiWorkDir(owner, repository) // clone working copy
cloneOrPullWorkingCopy(workDir, owner, repository) cloneOrPullWorkingCopy(workDir, owner, repository)
// delete file // delete file
new File(workDir, pageName + ".md").delete new File(workDir, pageName + ".md").delete
JGitUtil.withGit(workDir){ git => using(Git.open(workDir)){ git =>
git.rm.addFilepattern(pageName + ".md").call git.rm.addFilepattern(pageName + ".md").call
// commit and push // commit and push
git.commit.setAuthor(committer, mailAddress).setMessage(message).call git.commit.setAuthor(committer, mailAddress).setMessage(message).call
git.push.call git.push.call
}
} }
} }
} }
private def cloneOrPullWorkingCopy(workDir: File, owner: String, repository: String): Unit = { private def cloneOrPullWorkingCopy(workDir: File, owner: String, repository: String): Unit = {
if(!workDir.exists){ if(!workDir.exists){
val git = Git.cloneRepository
Git.cloneRepository .setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString)
.setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString) .setDirectory(workDir)
.setDirectory(workDir) .call
.call .getRepository
git.getRepository.close // close .git resources. .close
} else { } else using(Git.open(workDir)){ git =>
JGitUtil.withGit(workDir){ git => git.pull.call
git.pull.call
}
} }
} }

View File

@@ -6,7 +6,9 @@ import org.apache.commons.io.FileUtils
import javax.servlet.ServletContextEvent import javax.servlet.ServletContextEvent
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import util.Directory import util.Directory._
import util.ControlUtil._
import org.eclipse.jgit.api.Git
object AutoUpdate { object AutoUpdate {
@@ -26,15 +28,14 @@ object AutoUpdate {
*/ */
def update(conn: Connection): Unit = { def update(conn: Connection): Unit = {
val sqlPath = s"update/${majorVersion}_${minorVersion}.sql" val sqlPath = s"update/${majorVersion}_${minorVersion}.sql"
val in = Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)
if(in != null){ using(Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)){ in =>
val sql = IOUtils.toString(in, "UTF-8") if(in != null){
val stmt = conn.createStatement() val sql = IOUtils.toString(in, "UTF-8")
try { using(conn.createStatement()){ stmt =>
logger.debug(sqlPath + "=" + sql) logger.debug(sqlPath + "=" + sql)
stmt.executeUpdate(sql) stmt.executeUpdate(sql)
} finally { }
stmt.close()
} }
} }
} }
@@ -49,22 +50,24 @@ 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, 6),
Version(1, 5), Version(1, 5),
Version(1, 4), Version(1, 4),
new Version(1, 3){ new Version(1, 3){
override def update(conn: Connection): Unit = { override def update(conn: Connection): Unit = {
super.update(conn) super.update(conn)
// Fix wiki repository configuration // Fix wiki repository configuration
val rs = conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY") using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
while(rs.next){ while(rs.next){
val wikidir = Directory.getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")) using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
val repository = org.eclipse.jgit.api.Git.open(wikidir).getRepository defining(git.getRepository.getConfig){ config =>
val config = repository.getConfig if(!config.getBoolean("http", "receivepack", false)){
if(!config.getBoolean("http", "receivepack", false)){ config.setBoolean("http", null, "receivepack", true)
config.setBoolean("http", null, "receivepack", true) config.save
config.save }
}
}
} }
repository.close
} }
} }
}, },
@@ -81,7 +84,7 @@ object AutoUpdate {
/** /**
* The version file (GITBUCKET_HOME/version). * The version file (GITBUCKET_HOME/version).
*/ */
val versionFile = new File(Directory.GitBucketHome, "version") val versionFile = new File(GitBucketHome, "version")
/** /**
* Returns the current version from the version file. * Returns the current version from the version file.
@@ -112,27 +115,29 @@ class AutoUpdateListener extends org.h2.server.web.DbStarter {
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener]) private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
override def contextInitialized(event: ServletContextEvent): Unit = { override def contextInitialized(event: ServletContextEvent): Unit = {
event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${Directory.DatabaseHome}") event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome}")
super.contextInitialized(event) super.contextInitialized(event)
logger.debug("H2 started") logger.debug("H2 started")
logger.debug("Start schema update") logger.debug("Start schema update")
val conn = getConnection() defining(getConnection()){ conn =>
try { try {
val currentVersion = getCurrentVersion() defining(getCurrentVersion()){ currentVersion =>
if(currentVersion == headVersion){ if(currentVersion == headVersion){
logger.debug("No update") logger.debug("No update")
} else { } else {
versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn)) versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8") FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
conn.commit() conn.commit()
logger.debug("Updated from " + currentVersion.versionString + " to " + headVersion.versionString) logger.debug("Updated from " + currentVersion.versionString + " to " + headVersion.versionString)
} }
} catch { }
case ex: Throwable => { } catch {
logger.error("Failed to schema update", ex) case ex: Throwable => {
ex.printStackTrace() logger.error("Failed to schema update", ex)
conn.rollback() ex.printStackTrace()
conn.rollback()
}
} }
} }
logger.debug("End schema update") logger.debug("End schema update")

View File

@@ -4,6 +4,9 @@ import javax.servlet._
import javax.servlet.http._ import javax.servlet.http._
import service.{SystemSettingsService, AccountService, RepositoryService} import service.{SystemSettingsService, AccountService, RepositoryService}
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import util.Implicits._
import util.ControlUtil._
import util.Keys
/** /**
* Provides BASIC Authentication for [[servlet.GitRepositoryServlet]]. * Provides BASIC Authentication for [[servlet.GitRepositoryServlet]].
@@ -25,29 +28,30 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
} }
try { try {
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") defining(request.paths){ case Array(_, repositoryOwner, repositoryName, _*) =>
val repositoryOwner = paths(2) getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match {
val repositoryName = paths(3).replaceFirst("\\.git$", "") case Some(repository) => {
if(!request.getRequestURI.endsWith("/git-receive-pack") &&
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki", ""), "") match {
case Some(repository) => {
if(!request.getRequestURI.endsWith("/git-receive-pack") &&
!"service=git-receive-pack".equals(request.getQueryString) && !repository.repository.isPrivate){ !"service=git-receive-pack".equals(request.getQueryString) && !repository.repository.isPrivate){
chain.doFilter(req, wrappedResponse) chain.doFilter(req, wrappedResponse)
} else { } else {
request.getHeader("Authorization") match { request.getHeader("Authorization") match {
case null => requireAuth(response) case null => requireAuth(response)
case auth => decodeAuthHeader(auth).split(":") match { case auth => decodeAuthHeader(auth).split(":") match {
case Array(username, password) if(isWritableUser(username, password, repository)) => { case Array(username, password) if(isWritableUser(username, password, repository)) => {
request.setAttribute("USER_NAME", username) request.setAttribute(Keys.Request.UserName, username)
chain.doFilter(req, wrappedResponse) chain.doFilter(req, wrappedResponse)
}
case _ => requireAuth(response)
} }
case _ => requireAuth(response)
} }
} }
} }
case None => {
logger.debug(s"Repository ${repositoryOwner}/${repositoryName} is not found.")
response.sendError(HttpServletResponse.SC_NOT_FOUND)
}
} }
case None => response.sendError(HttpServletResponse.SC_NOT_FOUND)
} }
} catch { } catch {
case ex: Exception => { case ex: Exception => {

View File

@@ -9,8 +9,12 @@ 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.{JGitUtil, Directory} import util.{Keys, JGitUtil, Directory}
import util.ControlUtil._
import util.Implicits._
import service._ import service._
import WebHookService._
import org.eclipse.jgit.api.Git
/** /**
* Provides Git repository via HTTP. * Provides Git repository via HTTP.
@@ -51,31 +55,34 @@ 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("USER_NAME").asInstanceOf[String] val userName = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
logger.debug("requestURI: " + request.getRequestURI) logger.debug("requestURI: " + request.getRequestURI)
logger.debug("userName:" + userName) logger.debug("userName:" + userName)
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") defining(request.paths){ paths =>
val owner = paths(2) val owner = paths(1)
val repository = paths(3).replaceFirst("\\.git$", "") val repository = paths(2).replaceFirst("\\.git$", "")
val baseURL = request.getRequestURL.toString.replaceFirst("/git/.*", "")
logger.debug("repository:" + owner + "/" + repository)
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, userName)) logger.debug("repository:" + owner + "/" + repository)
receivePack logger.debug("baseURL:" + baseURL)
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, userName, baseURL))
receivePack
}
} }
} }
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String, userName: String) extends PostReceiveHook class CommitLogHook(owner: String, repository: String, userName: String, baseURL: String) extends PostReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService { with RepositoryService with AccountService with IssuesService with ActivityService 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 = {
JGitUtil.withGit(Directory.getRepositoryDir(owner, repository)) { git => using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
commands.asScala.foreach { command => commands.asScala.foreach { command =>
val commits = JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name) val commits = JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
val refName = command.getRefName.split("/") val refName = command.getRefName.split("/")
@@ -110,6 +117,16 @@ class CommitLogHook(owner: String, repository: String, userName: String) extends
case _ => case _ =>
} }
} }
// call web hook
val payload = WebHookPayload(
git,
command.getRefName,
getRepository(owner, repository, baseURL).get,
newCommits,
getAccountByUserName(owner).get)
callWebHook(owner, repository, payload)
} }
} }
// update repository last modified time. // update repository last modified time.

View File

@@ -21,9 +21,9 @@ class TransactionFilter extends Filter {
chain.doFilter(req, res) chain.doFilter(req, res)
} else { } else {
Database(req.getServletContext) withTransaction { Database(req.getServletContext) withTransaction {
logger.debug("TODO begin transaction") logger.debug("begin transaction")
chain.doFilter(req, res) chain.doFilter(req, res)
logger.debug("TODO end transaction") logger.debug("end transaction")
} }
} }
} }

View File

@@ -3,6 +3,8 @@ package util
import app.ControllerBase import app.ControllerBase
import service._ import service._
import RepositoryService.RepositoryInfo import RepositoryService.RepositoryInfo
import util.Implicits._
import util.ControlUtil._
/** /**
* Allows only oneself and administrators. * Allows only oneself and administrators.
@@ -13,11 +15,12 @@ trait OneselfAuthenticator { self: ControllerBase =>
private def authenticate(action: => Any) = { private def authenticate(action: => Any) = {
{ {
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") defining(request.paths){ paths =>
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action case Some(x) if(x.isAdmin) => action
case Some(x) if(paths(1) == x.userName) => action case Some(x) if(paths(0) == x.userName) => action
case _ => Unauthorized() case _ => Unauthorized()
}
} }
} }
} }
@@ -32,14 +35,15 @@ trait OwnerAuthenticator { self: ControllerBase with RepositoryService =>
private def authenticate(action: (RepositoryInfo) => Any) = { private def authenticate(action: (RepositoryInfo) => Any) = {
{ {
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") defining(request.paths){ paths =>
getRepository(paths(1), paths(2), baseUrl).map { repository => getRepository(paths(0), paths(1), baseUrl).map { repository =>
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(repository.owner == x.userName) => action(repository) case Some(x) if(repository.owner == x.userName) => action(repository)
case _ => Unauthorized() case _ => Unauthorized()
} }
} getOrElse NotFound() } getOrElse NotFound()
}
} }
} }
} }
@@ -87,15 +91,16 @@ trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService =
private def authenticate(action: (RepositoryInfo) => Any) = { private def authenticate(action: (RepositoryInfo) => Any) = {
{ {
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") defining(request.paths){ paths =>
getRepository(paths(1), paths(2), baseUrl).map { repository => getRepository(paths(0), paths(1), baseUrl).map { repository =>
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(paths(1) == x.userName) => action(repository) case Some(x) if(paths(0) == x.userName) => action(repository)
case Some(x) if(getCollaborators(paths(1), paths(2)).contains(x.userName)) => action(repository) case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository)
case _ => Unauthorized() case _ => Unauthorized()
} }
} getOrElse NotFound() } getOrElse NotFound()
}
} }
} }
} }
@@ -109,19 +114,20 @@ trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
private def authenticate(action: (RepositoryInfo) => Any) = { private def authenticate(action: (RepositoryInfo) => Any) = {
{ {
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") defining(request.paths){ paths =>
getRepository(paths(1), paths(2), baseUrl).map { repository => getRepository(paths(0), paths(1), baseUrl).map { repository =>
if(!repository.repository.isPrivate){ if(!repository.repository.isPrivate){
action(repository) action(repository)
} else { } else {
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(paths(1) == x.userName) => action(repository) case Some(x) if(paths(0) == x.userName) => action(repository)
case Some(x) if(getCollaborators(paths(1), paths(2)).contains(x.userName)) => action(repository) case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository)
case _ => Unauthorized() case _ => Unauthorized()
}
} }
} } getOrElse NotFound()
} getOrElse NotFound() }
} }
} }
} }
@@ -135,16 +141,17 @@ trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService =
private def authenticate(action: (RepositoryInfo) => Any) = { private def authenticate(action: (RepositoryInfo) => Any) = {
{ {
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") defining(request.paths){ paths =>
getRepository(paths(1), paths(2), baseUrl).map { repository => getRepository(paths(0), paths(1), baseUrl).map { repository =>
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(!repository.repository.isPrivate) => action(repository) case Some(x) if(!repository.repository.isPrivate) => action(repository)
case Some(x) if(paths(1) == x.userName) => action(repository) case Some(x) if(paths(0) == x.userName) => action(repository)
case Some(x) if(getCollaborators(paths(1), paths(2)).contains(x.userName)) => action(repository) case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository)
case _ => Unauthorized() case _ => Unauthorized()
} }
} getOrElse NotFound() } getOrElse NotFound()
}
} }
} }
} }

View File

@@ -0,0 +1,48 @@
package util
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.TreeWalk
/**
* Provides control facilities.
*/
object ControlUtil {
def defining[A, B](value: A)(f: A => B): B = f(value)
def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B =
try f(resource) finally {
if(resource != null){
try {
resource.close()
} catch {
case e: Throwable => // ignore
}
}
}
def using[T](git: Git)(f: Git => T): T =
try f(git) finally git.getRepository.close
def using[T](git1: Git, git2: Git)(f: (Git, Git) => T): T =
try f(git1, git2) finally {
git1.getRepository.close
git2.getRepository.close
}
def using[T](revWalk: RevWalk)(f: RevWalk => T): T =
try f(revWalk) finally revWalk.release()
def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T =
try f(treeWalk) finally treeWalk.release()
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

@@ -1,6 +1,7 @@
package util package util
import java.io.File import java.io.File
import util.ControlUtil._
/** /**
* Provides directories used by GitBucket. * Provides directories used by GitBucket.
@@ -21,17 +22,17 @@ object Directory {
/** /**
* Repository names of the specified user. * Repository names of the specified user.
*/ */
def getRepositories(owner: String): List[String] = { def getRepositories(owner: String): List[String] =
val dir = new File(s"${RepositoryHome}/${owner}") defining(new File(s"${RepositoryHome}/${owner}")){ dir =>
if(dir.exists){ if(dir.exists){
dir.listFiles.filter { file => dir.listFiles.filter { file =>
file.isDirectory && !file.getName.endsWith(".wiki.git") file.isDirectory && !file.getName.endsWith(".wiki.git")
}.map(_.getName.replaceFirst("\\.git$", "")).toList }.map(_.getName.replaceFirst("\\.git$", "")).toList
} else { } else {
Nil Nil
}
} }
}
/** /**
* Substance directory of the repository. * Substance directory of the repository.
*/ */

View File

@@ -4,19 +4,18 @@ import org.apache.commons.io.{IOUtils, FileUtils}
import java.net.URLConnection import java.net.URLConnection
import java.io.File import java.io.File
import org.apache.commons.compress.archivers.zip.{ZipArchiveEntry, ZipArchiveOutputStream} import org.apache.commons.compress.archivers.zip.{ZipArchiveEntry, ZipArchiveOutputStream}
import util.ControlUtil._
object FileUtil { object FileUtil {
def getMimeType(name: String): String = { def getMimeType(name: String): String =
val fileNameMap = URLConnection.getFileNameMap() defining(URLConnection.getFileNameMap()){ fileNameMap =>
val mimeType = fileNameMap.getContentTypeFor(name) fileNameMap.getContentTypeFor(name) match {
if(mimeType == null){ case null => "application/octet-stream"
"application/octeat-stream" case mimeType => mimeType
} else { }
mimeType
} }
}
def isImage(name: String): Boolean = getMimeType(name).startsWith("image/") def isImage(name: String): Boolean = getMimeType(name).startsWith("image/")
def isLarge(size: Long): Boolean = (size > 1024 * 1000) def isLarge(size: Long): Boolean = (size > 1024 * 1000)
@@ -36,21 +35,19 @@ object FileUtil {
} }
} }
val out = new ZipArchiveOutputStream(dest) using(new ZipArchiveOutputStream(dest)){ out =>
try {
addDirectoryToZip(out, dir, dir.getName) addDirectoryToZip(out, dir, dir.getName)
} finally {
IOUtils.closeQuietly(out)
} }
} }
def getExtension(name: String): String = { def getFileName(path: String): String = defining(path.lastIndexOf('/')){ i =>
val index = name.lastIndexOf('.') if(i >= 0) path.substring(i + 1) else path
if(index >= 0){
name.substring(index + 1)
} else {
""
}
} }
def getExtension(name: String): String =
name.lastIndexOf('.') match {
case i if(i >= 0) => name.substring(i + 1)
case _ => ""
}
} }

View File

@@ -1,6 +1,7 @@
package util package util
import scala.util.matching.Regex import scala.util.matching.Regex
import javax.servlet.http.{HttpSession, HttpServletRequest}
/** /**
* Provides some usable implicit conversions. * Provides some usable implicit conversions.
@@ -12,7 +13,7 @@ object Implicits {
def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition) def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition)
@scala.annotation.tailrec @scala.annotation.tailrec
private def split[A](list: Seq[A], result: Seq[Seq[A]] = Nil)(condition: (A, A) => Boolean): Seq[Seq[A]] = { private def split[A](list: Seq[A], result: Seq[Seq[A]] = Nil)(condition: (A, A) => Boolean): Seq[Seq[A]] =
list match { list match {
case x :: xs => { case x :: xs => {
xs.span(condition(x, _)) match { xs.span(condition(x, _)) match {
@@ -21,7 +22,6 @@ object Implicits {
} }
case Nil => result case Nil => result
} }
}
} }
implicit class RichString(value: String){ implicit class RichString(value: String){
@@ -43,4 +43,30 @@ object Implicits {
} }
} }
implicit class RichRequest(request: HttpServletRequest){
def paths: Array[String] = request.getRequestURI.substring(request.getContextPath.length + 1).split("/")
def hasQueryString: Boolean = request.getQueryString != null
def hasAttribute(name: String): Boolean = request.getAttribute(name) != null
}
implicit class RichSession(session: HttpSession){
def putAndGet[T](key: String, value: T): T = {
session.setAttribute(key, value)
value
}
def getAndRemove[T](key: String): Option[T] = {
val value = session.getAttribute(key).asInstanceOf[T]
if(value == null){
session.removeAttribute(key)
}
Option(value)
}
}
} }

View File

@@ -3,6 +3,7 @@ package util
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import util.Directory._ import util.Directory._
import util.StringUtil._ import util.StringUtil._
import util.ControlUtil._
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
import org.eclipse.jgit.lib._ import org.eclipse.jgit.lib._
import org.eclipse.jgit.revwalk._ import org.eclipse.jgit.revwalk._
@@ -68,29 +69,17 @@ object JGitUtil {
rev.getFullMessage, rev.getFullMessage,
rev.getParents().map(_.name).toList) rev.getParents().map(_.name).toList)
val summary = { val summary = defining(fullMessage.trim.indexOf("\n")){ i =>
val i = fullMessage.trim.indexOf("\n") defining(if(i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage){ firstLine =>
val firstLine = if(i >= 0){ if(firstLine.length > shortMessage.length) shortMessage else firstLine
fullMessage.trim.substring(0, i).trim
} else {
fullMessage
}
if(firstLine.length > shortMessage.length){
shortMessage
} else {
firstLine
} }
} }
val description = { val description = defining(fullMessage.trim.indexOf("\n")){ i =>
val i = fullMessage.trim.indexOf("\n") optionIf(i >= 0){
if(i >= 0){
Some(fullMessage.trim.substring(i).trim) Some(fullMessage.trim.substring(i).trim)
} else {
None
} }
} }
} }
case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String]) case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String])
@@ -112,24 +101,6 @@ object JGitUtil {
*/ */
case class TagInfo(name: String, time: Date, id: String) case class TagInfo(name: String, time: Date, id: String)
/**
* Use this method to use the Git object.
* Repository resources are released certainly after processing.
*/
def withGit[T](dir: java.io.File)(f: Git => T): T = withGit(Git.open(dir))(f)
/**
* Use this method to use the Git object.
* Repository resources are released certainly after processing.
*/
def withGit[T](git: Git)(f: Git => T): T = {
try {
f(git)
} finally {
git.getRepository.close
}
}
/** /**
* Returns RevCommit from the commit or tag id. * Returns RevCommit from the commit or tag id.
* *
@@ -141,7 +112,7 @@ object JGitUtil {
val revWalk = new RevWalk(git.getRepository) val revWalk = new RevWalk(git.getRepository)
val revCommit = revWalk.parseAny(objectId) match { val revCommit = revWalk.parseAny(objectId) match {
case r: RevTag => revWalk.parseCommit(r.getObject) case r: RevTag => revWalk.parseCommit(r.getObject)
case _ => revWalk.parseCommit(objectId) case _ => revWalk.parseCommit(objectId)
} }
revWalk.dispose revWalk.dispose
revCommit revCommit
@@ -151,7 +122,7 @@ object JGitUtil {
* Returns the repository information. It contains branch names and tag names. * Returns the repository information. It contains branch names and tag names.
*/ */
def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = { def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = {
withGit(getRepositoryDir(owner, repository)){ git => using(Git.open(getRepositoryDir(owner, repository))){ git =>
try { try {
// get commit count // get commit count
val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(1000).sum val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(1000).sum
@@ -188,45 +159,43 @@ object JGitUtil {
* @return HTML of the file list * @return HTML of the file list
*/ */
def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = { def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = {
val revWalk = new RevWalk(git.getRepository)
val objectId = git.getRepository.resolve(revision)
val revCommit = revWalk.parseCommit(objectId)
val treeWalk = new TreeWalk(git.getRepository)
treeWalk.addTree(revCommit.getTree)
if(path != "."){
treeWalk.setRecursive(true)
treeWalk.setFilter(new TreeFilter(){
var stopRecursive = false
def include(walker: TreeWalk): Boolean = {
val targetPath = walker.getPathString
if((path + "/").startsWith(targetPath)){
true
} else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf("/") < 0){
stopRecursive = true
treeWalk.setRecursive(false)
true
} else {
false
}
}
def shouldBeRecursive(): Boolean = !stopRecursive
override def clone: TreeFilter = return this
})
}
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String)] val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String)]
while (treeWalk.next()) { using(new RevWalk(git.getRepository)){ revWalk =>
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString)) val objectId = git.getRepository.resolve(revision)
val revCommit = revWalk.parseCommit(objectId)
using(new TreeWalk(git.getRepository)){ treeWalk =>
treeWalk.addTree(revCommit.getTree)
if(path != "."){
treeWalk.setRecursive(true)
treeWalk.setFilter(new TreeFilter(){
var stopRecursive = false
def include(walker: TreeWalk): Boolean = {
val targetPath = walker.getPathString
if((path + "/").startsWith(targetPath)){
true
} else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf("/") < 0){
stopRecursive = true
treeWalk.setRecursive(false)
true
} else {
false
}
}
def shouldBeRecursive(): Boolean = !stopRecursive
override def clone: TreeFilter = return this
})
}
while (treeWalk.next()) {
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString))
}
}
} }
treeWalk.release
revWalk.dispose
val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision) val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision)
list.map { case (objectId, fileMode, path, name) => list.map { case (objectId, fileMode, path, name) =>
@@ -270,25 +239,23 @@ object JGitUtil {
case _ => (logs, i.hasNext) case _ => (logs, i.hasNext)
} }
val revWalk = new RevWalk(git.getRepository) using(new RevWalk(git.getRepository)){ revWalk =>
val objectId = git.getRepository.resolve(revision) defining(git.getRepository.resolve(revision)){ objectId =>
if(objectId == null){ if(objectId == null){
Left(s"${revision} can't be resolved.") Left(s"${revision} can't be resolved.")
} else { } else {
revWalk.markStart(revWalk.parseCommit(objectId)) revWalk.markStart(revWalk.parseCommit(objectId))
if(path.nonEmpty){ if(path.nonEmpty){
revWalk.setRevFilter(new RevFilter(){ revWalk.setRevFilter(new RevFilter(){
def include(walk: RevWalk, commit: RevCommit): Boolean = { def include(walk: RevWalk, commit: RevCommit): Boolean = {
getDiffs(git, commit.getName, false)._1.find(_.newPath == path).nonEmpty getDiffs(git, commit.getName, false)._1.find(_.newPath == path).nonEmpty
}
override def clone(): RevFilter = this
})
} }
override def clone(): RevFilter = this Right(getCommitLog(revWalk.iterator, 0, Nil))
}) }
} }
val commits = getCommitLog(revWalk.iterator, 0, Nil)
revWalk.release
Right(commits)
} }
} }
@@ -308,13 +275,10 @@ object JGitUtil {
case false => logs case false => logs
} }
val revWalk = new RevWalk(git.getRepository) using(new RevWalk(git.getRepository)){ revWalk =>
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(begin))) revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(begin)))
getCommitLog(revWalk.iterator, Nil).reverse
val commits = getCommitLog(revWalk.iterator, Nil) }
revWalk.release
commits.reverse
} }
@@ -370,11 +334,8 @@ object JGitUtil {
if(large == false && FileUtil.isLarge(loader.getSize)){ if(large == false && FileUtil.isLarge(loader.getSize)){
None None
} else { } else {
val db = git.getRepository.getObjectDatabase using(git.getRepository.getObjectDatabase){ db =>
try {
Some(db.open(id).getBytes) Some(db.open(id).getBytes)
} finally {
db.close
} }
} }
} catch { } catch {
@@ -392,34 +353,32 @@ object JGitUtil {
case _ => logs case _ => logs
} }
val revWalk = new RevWalk(git.getRepository) using(new RevWalk(git.getRepository)){ revWalk =>
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(id))) revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(id)))
val commits = getCommitLog(revWalk.iterator, Nil)
val commits = getCommitLog(revWalk.iterator, Nil) val revCommit = commits(0)
revWalk.release
val revCommit = commits(0)
if(commits.length >= 2){
// not initial commit
val oldCommit = commits(1)
(getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName))
} else { if(commits.length >= 2){
// initial commit // not initial commit
val walk = new TreeWalk(git.getRepository) val oldCommit = commits(1)
walk.addTree(revCommit.getTree) (getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName))
val buffer = new scala.collection.mutable.ListBuffer[DiffInfo]()
while(walk.next){ } else {
buffer.append((if(!fetchContent){ // initial commit
DiffInfo(ChangeType.ADD, null, walk.getPathString, None, None) using(new TreeWalk(git.getRepository)){ treeWalk =>
} else { treeWalk.addTree(revCommit.getTree)
DiffInfo(ChangeType.ADD, null, walk.getPathString, None, val buffer = new scala.collection.mutable.ListBuffer[DiffInfo]()
JGitUtil.getContent(git, walk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray)) while(treeWalk.next){
})) buffer.append((if(!fetchContent){
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None)
} else {
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None,
JGitUtil.getContent(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray))
}))
}
(buffer.toList, None)
}
} }
walk.release
(buffer.toList, None)
} }
} }
@@ -447,67 +406,49 @@ object JGitUtil {
/** /**
* Returns the list of branch names of the specified commit. * Returns the list of branch names of the specified commit.
*/ */
def getBranchesOfCommit(git: Git, commitId: String): List[String] = { def getBranchesOfCommit(git: Git, commitId: String): List[String] =
val walk = new org.eclipse.jgit.revwalk.RevWalk(git.getRepository) using(new RevWalk(git.getRepository)){ revWalk =>
try { defining(revWalk.parseCommit(git.getRepository.resolve(commitId + "^0"))){ commit =>
val commit = walk.parseCommit(git.getRepository.resolve(commitId + "^0")) git.getRepository.getAllRefs.entrySet.asScala.filter { e =>
(e.getKey.startsWith(Constants.R_HEADS) && revWalk.isMergedInto(commit, revWalk.parseCommit(e.getValue.getObjectId)))
git.getRepository.getAllRefs.entrySet.asScala.filter { e => }.map { e =>
(e.getKey.startsWith(Constants.R_HEADS) && walk.isMergedInto(commit, walk.parseCommit(e.getValue.getObjectId))) e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_HEADS.length)
}.map { e => }.toList.sorted
e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_HEADS.length) }
}.toList.sorted
} finally {
walk.release
} }
}
/** /**
* Returns the list of tags of the specified commit. * Returns the list of tags of the specified commit.
*/ */
def getTagsOfCommit(git: Git, commitId: String): List[String] = { def getTagsOfCommit(git: Git, commitId: String): List[String] =
val walk = new org.eclipse.jgit.revwalk.RevWalk(git.getRepository) using(new RevWalk(git.getRepository)){ revWalk =>
try { defining(revWalk.parseCommit(git.getRepository.resolve(commitId + "^0"))){ commit =>
val commit = walk.parseCommit(git.getRepository.resolve(commitId + "^0")) git.getRepository.getAllRefs.entrySet.asScala.filter { e =>
(e.getKey.startsWith(Constants.R_TAGS) && revWalk.isMergedInto(commit, revWalk.parseCommit(e.getValue.getObjectId)))
git.getRepository.getAllRefs.entrySet.asScala.filter { e => }.map { e =>
(e.getKey.startsWith(Constants.R_TAGS) && walk.isMergedInto(commit, walk.parseCommit(e.getValue.getObjectId))) e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_TAGS.length)
}.map { e => }.toList.sorted.reverse
e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_TAGS.length) }
}.toList.sorted.reverse
} finally {
walk.release
} }
}
def initRepository(dir: java.io.File): Unit = { def initRepository(dir: java.io.File): Unit =
val repository = new RepositoryBuilder().setGitDir(dir).setBare.build using(new RepositoryBuilder().setGitDir(dir).setBare.build){ repository =>
try {
repository.create repository.create
setReceivePack(repository) setReceivePack(repository)
} finally {
repository.close
} }
}
def cloneRepository(from: java.io.File, to: java.io.File): Unit = { def cloneRepository(from: java.io.File, to: java.io.File): Unit =
val git = Git.cloneRepository.setURI(from.toURI.toString).setDirectory(to).setBare(true).call using(Git.cloneRepository.setURI(from.toURI.toString).setDirectory(to).setBare(true).call){ git =>
try {
setReceivePack(git.getRepository) setReceivePack(git.getRepository)
} finally {
git.getRepository.close
} }
}
def isEmpty(git: Git): Boolean = git.getRepository.resolve(Constants.HEAD) == null def isEmpty(git: Git): Boolean = git.getRepository.resolve(Constants.HEAD) == null
private def setReceivePack(repository: org.eclipse.jgit.lib.Repository): Unit = { private def setReceivePack(repository: org.eclipse.jgit.lib.Repository): Unit =
val config = repository.getConfig defining(repository.getConfig){ config =>
config.setBoolean("http", null, "receivepack", true) config.setBoolean("http", null, "receivepack", true)
config.save config.save
} }
def getDefaultBranch(git: Git, repository: RepositoryService.RepositoryInfo, def getDefaultBranch(git: Git, repository: RepositoryService.RepositoryInfo,
revstr: String = ""): Option[(ObjectId, String)] = { revstr: String = ""): Option[(ObjectId, String)] = {

View File

@@ -0,0 +1,72 @@
package util
/**
* Define key strings for request attributes, session attributes or flash attributes.
*/
object Keys {
/**
* Define session keys.
*/
object Session {
/**
* Session key for the logged in account information.
*/
val LoginAccount = "LOGIN_ACCOUNT"
/**
* Session key for the redirect URL.
*/
val Redirect = "REDIRECT"
/**
* Session key for the issue search condition in dashboard.
*/
val DashboardIssues = "dashboard/issues"
/**
* Session key for the pull request search condition in dashboard.
*/
val DashboardPulls = "dashboard/pulls"
/**
* Generate session key for the issue search condition.
*/
def Issues(owner: String, name: String) = s"${owner}/${name}/issues"
/**
* Generate session key for the pull request search condition.
*/
def Pulls(owner: String, name: String) = s"${owner}/${name}/pulls"
/**
* Generate session key for the upload filename.
*/
def Upload(fileId: String) = s"upload_${fileId}"
}
/**
* Define request keys.
*/
object Request {
/**
* Request key for the Ajax request flag.
*/
val Ajax = "AJAX"
/**
* Request key for the username which is used during Git repository access.
*/
val UserName = "USER_NAME"
/**
* Generate request key for the request cache.
*/
def Cache(key: String) = s"cache.${key}"
}
}

View File

@@ -1,10 +1,9 @@
package util package util
import service.SystemSettingsService.Ldap import util.ControlUtil._
import service.SystemSettingsService import service.SystemSettingsService
import com.novell.ldap._ import com.novell.ldap._
import service.SystemSettingsService.Ldap import service.SystemSettingsService.Ldap
import scala.Some
import scala.annotation.tailrec import scala.annotation.tailrec
/** /**
@@ -96,12 +95,10 @@ object LDAPUtil {
} }
} }
private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] = { private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] =
val results = conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false) defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false)){ results =>
if (results.hasMore) { optionIf (results.hasMore) {
Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue) Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
} else { }
None
} }
}
} }

View File

@@ -2,6 +2,7 @@ package util
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.locks.{ReentrantLock, Lock} import java.util.concurrent.locks.{ReentrantLock, Lock}
import util.ControlUtil._
object LockUtil { object LockUtil {
@@ -23,8 +24,7 @@ object LockUtil {
/** /**
* Synchronizes a given function which modifies the working copy of the wiki repository. * Synchronizes a given function which modifies the working copy of the wiki repository.
*/ */
def lock[T](key: String)(f: => T): T = { def lock[T](key: String)(f: => T): T = defining(getLockObject(key)){ lock =>
val lock = getLockObject(key)
try { try {
lock.lock() lock.lock()
f f

View File

@@ -65,6 +65,8 @@ class Mailer(private val smtp: Smtp) extends Notifier {
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
(msg: String => String)(implicit context: Context) = { (msg: String => String)(implicit context: Context) = {
val database = Database(context.request.getServletContext)
val f = future { val f = future {
val email = new HtmlEmail val email = new HtmlEmail
email.setHostName(smtp.host) email.setHostName(smtp.host)
@@ -79,7 +81,7 @@ class Mailer(private val smtp: Smtp) extends Notifier {
email.setHtmlMsg(msg(view.Markdown.toHtml(content, r, false, true))) email.setHtmlMsg(msg(view.Markdown.toHtml(content, r, false, true)))
// TODO Can we use the Database Session in other than Transaction Filter? // TODO Can we use the Database Session in other than Transaction Filter?
Database(context.request.getServletContext) withSession { database withSession {
getIssue(r.owner, r.name, issueId.toString) foreach { issue => getIssue(r.owner, r.name, issueId.toString) foreach { issue =>
email.setSubject(s"[${r.name}] ${issue.title} (#${issueId})") email.setSubject(s"[${r.name}] ${issue.title} (#${issueId})")
recipients(issue) { recipients(issue) {

View File

@@ -2,14 +2,15 @@ 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._
object StringUtil { object StringUtil {
def sha1(value: String): String = { def sha1(value: String): String =
val md = java.security.MessageDigest.getInstance("SHA-1") defining(java.security.MessageDigest.getInstance("SHA-1")){ md =>
md.update(value.getBytes) md.update(value.getBytes)
md.digest.map(b => "%02x".format(b)).mkString md.digest.map(b => "%02x".format(b)).mkString
} }
def md5(value: String): String = { def md5(value: String): String = {
val md = java.security.MessageDigest.getInstance("MD5") val md = java.security.MessageDigest.getInstance("MD5")
@@ -28,13 +29,13 @@ object StringUtil {
def convertFromByteArray(content: Array[Byte]): String = new String(content, detectEncoding(content)) def convertFromByteArray(content: Array[Byte]): String = new String(content, detectEncoding(content))
def detectEncoding(content: Array[Byte]): String = { def detectEncoding(content: Array[Byte]): String =
val detector = new UniversalDetector(null) defining(new UniversalDetector(null)){ detector =>
detector.handleData(content, 0, content.length) detector.handleData(content, 0, content.length)
detector.dataEnd() detector.dataEnd()
detector.getDetectedCharset match { detector.getDetectedCharset match {
case null => "UTF-8" case null => "UTF-8"
case e => e case e => e
}
} }
}
} }

View File

@@ -25,10 +25,7 @@ trait Validations {
*/ */
def date(constraints: Constraint*): SingleValueType[java.util.Date] = def date(constraints: Constraint*): SingleValueType[java.util.Date] =
new SingleValueType[java.util.Date]((pattern("\\d{4}-\\d{2}-\\d{2}") +: constraints): _*){ new SingleValueType[java.util.Date]((pattern("\\d{4}-\\d{2}-\\d{2}") +: constraints): _*){
def convert(value: String): java.util.Date = { def convert(value: String): java.util.Date = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(value)
val formatter = new java.text.SimpleDateFormat("yyyy-MM-dd")
formatter.parse(value)
}
} }
} }

View File

@@ -52,7 +52,7 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
} }
class GitBucketVerbatimSerializer extends VerbatimSerializer { class GitBucketVerbatimSerializer extends VerbatimSerializer {
def serialize(node: VerbatimNode, printer: Printer) { def serialize(node: VerbatimNode, printer: Printer): Unit = {
printer.println.print("<pre") printer.println.print("<pre")
if (!StringUtils.isEmpty(node.getType)) { if (!StringUtils.isEmpty(node.getType)) {
printer.print(" class=").print('"').print("prettyprint ").print(node.getType).print('"') printer.print(" class=").print('"').print("prettyprint ").print(node.getType).print('"')
@@ -98,11 +98,11 @@ class GitBucketHtmlSerializer(
} }
} }
private def printAttribute(name: String, value: String) { private def printAttribute(name: String, value: String): Unit = {
printer.print(' ').print(name).print('=').print('"').print(value).print('"') printer.print(' ').print(name).print('=').print('"').print(value).print('"')
} }
override def visit(node: TextNode) { 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

View File

@@ -31,9 +31,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
* Converts Markdown of Wiki pages to HTML. * Converts Markdown of Wiki pages to HTML.
*/ */
def markdown(value: String, repository: service.RepositoryService.RepositoryInfo, def markdown(value: String, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = { enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink)) Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink))
}
/** /**
* Returns &lt;img&gt; which displays the avatar icon. * Returns &lt;img&gt; which displays the avatar icon.
@@ -81,14 +80,12 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
/** /**
* Generates the url to the account page. * Generates the url to the account page.
*/ */
def url(userName: String)(implicit context: app.Context): String = def url(userName: String)(implicit context: app.Context): String = s"${context.path}/${userName}"
s"${context.path}/${userName}"
/** /**
* Returns the url to the root of assets. * Returns the url to the root of assets.
*/ */
def assets(implicit context: app.Context): String = def assets(implicit context: app.Context): String = s"${context.path}/assets"
s"${context.path}/assets"
def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime

View File

@@ -13,14 +13,15 @@
<div class="span6"> <div class="span6">
@if(account.isEmpty){ @if(account.isEmpty){
<fieldset> <fieldset>
<label for="userName"><strong>User name</strong></label> <label for="userName" class="strong">User name</label>
<input type="text" name="userName" id="userName" value=""/> <input type="text" name="userName" id="userName" value=""/>
<span id="error-userName" class="error"></span> <span id="error-userName" class="error"></span>
</fieldset> </fieldset>
} }
@if(account.map(_.password.nonEmpty).getOrElse(true)){ @if(account.map(_.password.nonEmpty).getOrElse(true)){
<fieldset> <fieldset>
<label for="password"><strong>Password</strong> <label for="password" class="strong">
Password
@if(account.nonEmpty){ @if(account.nonEmpty){
(Input to change password) (Input to change password)
} }
@@ -30,19 +31,19 @@
</fieldset> </fieldset>
} }
<fieldset> <fieldset>
<label for="mailAddress"><strong>Mail Address</strong></label> <label for="mailAddress" class="strong">Mail Address</label>
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/> <input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
<span id="error-mailAddress" class="error"></span> <span id="error-mailAddress" class="error"></span>
</fieldset> </fieldset>
<fieldset> <fieldset>
<label for="url"><strong>URL (Optional)</strong></label> <label for="url" class="strong">URL (Optional)</label>
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/> <input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
<span id="error-url" class="error"></span> <span id="error-url" class="error"></span>
</fieldset> </fieldset>
</div> </div>
<div class="span6"> <div class="span6">
<fieldset> <fieldset>
<label for="avatar"><strong>Image (Optional)</strong></label> <label for="avatar" class="strong">Image (Optional)</label>
@helper.html.uploadavatar(account) @helper.html.uploadavatar(account)
</fieldset> </fieldset>
</div> </div>

View File

@@ -11,22 +11,22 @@
<!--====================================================================--> <!--====================================================================-->
<!-- Account registration --> <!-- Account registration -->
<!--====================================================================--> <!--====================================================================-->
<label><strong>Account registration</strong></label> <label class="strong">Account registration</label>
<fieldset> <fieldset>
<label> <label>
<input type="radio" name="allowAccountRegistration" value="true"@if(settings.allowAccountRegistration){ checked}> <input type="radio" name="allowAccountRegistration" value="true"@if(settings.allowAccountRegistration){ checked}>
<strong>Allow</strong> - Users can create account by themselves. <span class="strong">Allow</span> - Users can create account by themselves.
</label> </label>
<label> <label>
<input type="radio" name="allowAccountRegistration" value="false"@if(!settings.allowAccountRegistration){ checked}> <input type="radio" name="allowAccountRegistration" value="false"@if(!settings.allowAccountRegistration){ checked}>
<strong>Deny</strong> - Only administrators can create account. <span class="strong">Deny</span> - Only administrators can create account.
</label> </label>
</fieldset> </fieldset>
<!--====================================================================--> <!--====================================================================-->
<!-- Services --> <!-- Services -->
<!--====================================================================--> <!--====================================================================-->
<hr> <hr>
<label><strong>Services</strong></label> <label class="strong">Services</label>
<fieldset> <fieldset>
<label> <label>
<input type="checkbox" name="gravatar"@if(settings.gravatar){ checked}/> <input type="checkbox" name="gravatar"@if(settings.gravatar){ checked}/>
@@ -37,7 +37,7 @@
<!-- Authentication --> <!-- Authentication -->
<!--====================================================================--> <!--====================================================================-->
<hr> <hr>
<label><strong>Authentication</strong></label> <label class="strong">Authentication</label>
<fieldset> <fieldset>
<label> <label>
<input type="checkbox" id="ldapAuthentication" name="ldapAuthentication"@if(settings.ldap){ checked}/> <input type="checkbox" id="ldapAuthentication" name="ldapAuthentication"@if(settings.ldap){ checked}/>
@@ -99,7 +99,7 @@
<!-- Notification email --> <!-- Notification email -->
<!--====================================================================--> <!--====================================================================-->
<hr> <hr>
<label><strong>Notification email</strong></label> <label class="strong">Notification email</label>
<fieldset> <fieldset>
<label> <label>
<input type="checkbox" id="notification" name="notification"@if(settings.notification){ checked}/> <input type="checkbox" id="notification" name="notification"@if(settings.notification){ checked}/>

View File

@@ -7,23 +7,23 @@
<div class="row-fluid"> <div class="row-fluid">
<div class="span7"> <div class="span7">
<fieldset> <fieldset>
<label for="groupName"><strong>Group name</strong></label> <label for="groupName" class="strong">Group name</label>
<span id="error-groupName" class="error"></span> <span id="error-groupName" class="error"></span>
<input type="text" name="groupName" id="groupName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/> <input type="text" name="groupName" id="groupName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
</fieldset> </fieldset>
<fieldset> <fieldset>
<label><strong>URL (Optional)</strong></label> <label class="strong">URL (Optional)</label>
<span id="error-url" class="error"></span> <span id="error-url" class="error"></span>
<input type="text" name="url" id="url" style="width: 300px;" value="@account.map(_.url)"/> <input type="text" name="url" id="url" style="width: 300px;" value="@account.map(_.url)"/>
</fieldset> </fieldset>
<fieldset> <fieldset>
<label for="avatar"><strong>Image (Optional)</strong></label> <label for="avatar" class="strong">Image (Optional)</label>
@helper.html.uploadavatar(account) @helper.html.uploadavatar(account)
</fieldset> </fieldset>
</div> </div>
<div class="span5"> <div class="span5">
<fieldset> <fieldset>
<label><strong>Members</strong></label> <label class="strong">Members</label>
<ul id="members" class="collaborator"> <ul id="members" class="collaborator">
@members.map { userName => @members.map { userName =>
<li data-name="@userName"> <li data-name="@userName">
@@ -32,7 +32,7 @@
</li> </li>
} }
</ul> </ul>
<input type="text" id="memberName" style="width: 200px; margin-bottom: 0px;"/> @helper.html.account("memberName", 200)
<input type="button" class="btn" value="Add" id="addMember"/> <input type="button" class="btn" value="Add" id="addMember"/>
<input type="hidden" id="memberNames" name="memberNames" value="@members.mkString(",")"/> <input type="hidden" id="memberNames" name="memberNames" value="@members.mkString(",")"/>
<div> <div>
@@ -50,15 +50,6 @@
} }
<script> <script>
$(function(){ $(function(){
$('#memberName').typeahead({
source: function (query, process) {
return $.get('@path/_user/proposals', { query: query },
function (data) {
return process(data.options);
});
}
});
$('#addMember').click(function(){ $('#addMember').click(function(){
$('#error-memberName').text(''); $('#error-memberName').text('');
var userName = $('#memberName').val(); var userName = $('#memberName').val();

View File

@@ -6,14 +6,14 @@
<div class="row-fluid"> <div class="row-fluid">
<div class="span6"> <div class="span6">
<fieldset> <fieldset>
<label for="userName"><strong>Username</strong></label> <label for="userName" class="strong">Username</label>
<span id="error-userName" class="error"></span> <span id="error-userName" class="error"></span>
<input type="text" name="userName" id="userName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/> <input type="text" name="userName" id="userName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
</fieldset> </fieldset>
@if(account.map(_.password.nonEmpty).getOrElse(true)){ @if(account.map(_.password.nonEmpty).getOrElse(true)){
<fieldset> <fieldset>
<label for="password"> <label for="password" class="strong">
<strong>Password</strong> Password
@if(account.isDefined){ @if(account.isDefined){
(Input to change password) (Input to change password)
} }
@@ -23,12 +23,12 @@
</fieldset> </fieldset>
} }
<fieldset> <fieldset>
<label for="mailAddress"><strong>Mail Address</strong></label> <label for="mailAddress" class="strong">Mail Address</label>
<span id="error-mailAddress" class="error"></span> <span id="error-mailAddress" class="error"></span>
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/> <input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
</fieldset> </fieldset>
<fieldset> <fieldset>
<label><strong>User Type</strong></label> <label class="strong">User Type</label>
<label for="userType_Normal"> <label for="userType_Normal">
<input type="radio" name="isAdmin" id="userType_Normal" value="false"@if(account.isEmpty || !account.get.isAdmin){ checked}/> Normal <input type="radio" name="isAdmin" id="userType_Normal" value="false"@if(account.isEmpty || !account.get.isAdmin){ checked}/> Normal
</label> </label>
@@ -37,14 +37,14 @@
</label> </label>
</fieldset> </fieldset>
<fieldset> <fieldset>
<label><strong>URL (Optional)</strong></label> <label class="strong">URL (Optional)</label>
<span id="error-url" class="error"></span> <span id="error-url" class="error"></span>
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/> <input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
</fieldset> </fieldset>
</div> </div>
<div class="span6"> <div class="span6">
<fieldset> <fieldset>
<label for="avatar"><strong>Image (Optional)</strong></label> <label for="avatar" class="strong">Image (Optional)</label>
@helper.html.uploadavatar(account) @helper.html.uploadavatar(account)
</fieldset> </fieldset>
</div> </div>

View File

@@ -48,10 +48,8 @@
<a href="@url(repository)/settings">Settings</a> <a href="@url(repository)/settings">Settings</a>
</th> </th>
} }
</tr> </tr>
</table> </table>
<form method="POST" id="repository_form">
</form>
<script type="text/javascript"> <script type="text/javascript">
$(function(){ $(function(){
$('table.global-nav th.box-header').click(function(){ $('table.global-nav th.box-header').click(function(){

View File

@@ -0,0 +1,15 @@
@(id: String, width: Int)(implicit context: app.Context)
@import context._
<input type="text" name="@id" id="@id" style="width: @{width}px; margin-bottom: 0px;"/>
<script>
$(function(){
$('#@id').typeahead({
source: function (query, process) {
return $.get('@path/_user/proposals', { query: query },
function (data) {
return process(data.options);
});
}
});
});
</script>

View File

@@ -24,10 +24,12 @@
if(i == 3){ if(i == 3){
<div>...</div> <div>...</div>
} else { } else {
<div> if(commit.nonEmpty){
<a href={s"${path}/${activity.userName}/${activity.repositoryName}/commit/${commit.substring(0, 40)}"} class="monospace">{commit.substring(0, 7)}</a> <div>
<span>{commit.substring(41)}</span> <a href={s"${path}/${activity.userName}/${activity.repositoryName}/commit/${commit. substring(0, 40)}"} class="monospace">{commit.substring(0, 7)}</a>
</div> <span>{commit.substring(41)}</span>
</div>
}
} }
}} }}
</div> </div>

View File

@@ -1,10 +1,39 @@
@(diffs: Seq[util.JGitUtil.DiffInfo], @(diffs: Seq[util.JGitUtil.DiffInfo],
repository: service.RepositoryService.RepositoryInfo, repository: service.RepositoryService.RepositoryInfo,
newCommitId: Option[String], newCommitId: Option[String],
oldCommitId: Option[String])(implicit context: app.Context) oldCommitId: Option[String],
showIndex: Boolean)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@import org.eclipse.jgit.diff.DiffEntry.ChangeType @import org.eclipse.jgit.diff.DiffEntry.ChangeType
@if(showIndex){
<div>
<div class="pull-right" style="margin-bottom: 10px;">
<input id="toggle-file-list" type="button" class="btn" value="Show file list"/>
</div>
Showing @diffs.size changed @plural(diffs.size, "file")
</div>
<ul id="commit-file-list" style="display: none;">
@diffs.zipWithIndex.map { case (diff, i) =>
<li@if(i > 0){ class="border"}>
<a href="#diff-@i">
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
<img src="@assets/common/images/diff_move.png"/> @diff.oldPath -> @diff.newPath
}
@if(diff.changeType == ChangeType.ADD){
<img src="@assets/common/images/diff_add.png"/> @diff.newPath
}
@if(diff.changeType == ChangeType.MODIFY){
<img src="@assets/common/images/diff_edit.png"/> @diff.newPath
}
@if(diff.changeType == ChangeType.DELETE){
<img src="@assets/common/images/diff_delete.png"/> @diff.oldPath
}
</a>
</li>
}
</ul>
}
@diffs.zipWithIndex.map { case (diff, i) => @diffs.zipWithIndex.map { case (diff, i) =>
<a name="diff-@i"></a> <a name="diff-@i"></a>
<table class="table table-bordered"> <table class="table table-bordered">
@@ -103,6 +132,17 @@ function diffUsingJS(oldTextId, newTextId, outputId) {
} }
$(function(){ $(function(){
@if(showIndex){
$('#toggle-file-list').click(function(){
$('#commit-file-list').toggle();
if($(this).val() == 'Show file list'){
$(this).val('Hide file list');
} else {
$(this).val('Show file list');
}
});
}
@diffs.zipWithIndex.map { case (diff, i) => @diffs.zipWithIndex.map { case (diff, i) =>
@if(diff.newContent != None || diff.oldContent != None){ @if(diff.newContent != None || diff.oldContent != None){
if($('#oldText-@i').length > 0){ if($('#oldText-@i').length > 0){

View File

@@ -1,13 +1,13 @@
@(buttonValue: String = "", prefix: String = "")(body: Html) @(value: String = "", prefix: String = "", mini: Boolean = true, style: String = "")(body: Html)
<div class="btn-group"> <div class="btn-group"@if(style.nonEmpty){ style="@style"}>
<button class="btn btn-mini dropdown-toggle" data-toggle="dropdown"> <button class="btn dropdown-toggle@if(mini){ btn-mini}" data-toggle="dropdown">
@if(buttonValue.isEmpty){ @if(value.isEmpty){
<i class="icon-cog"></i> <i class="icon-cog"></i>
} else { } else {
@if(prefix.nonEmpty){ @if(prefix.nonEmpty){
<span class="muted">@prefix:</span> <span class="muted">@prefix:</span>
} }
<strong>@buttonValue</strong> <span class="strong">@value</span>
} }
<span class="caret"></span> <span class="caret"></span>
</button> </button>

View File

@@ -32,9 +32,9 @@
<tr> <tr>
<td> <td>
@if(repository.owner == loginAccount.get.userName){ @if(repository.owner == loginAccount.get.userName){
<a href="@url(repository)"><strong>@repository.name</strong></a> <a href="@url(repository)"><span class="strong">@repository.name</span></a>
} else { } else {
<a href="@url(repository)">@repository.owner/<strong>@repository.name</strong></a> <a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
} }
</td> </td>
</tr> </tr>
@@ -57,7 +57,7 @@
@recentRepositories.map { repository => @recentRepositories.map { repository =>
<tr> <tr>
<td> <td>
<a href="@url(repository)">@repository.owner/<strong>@repository.name</strong></a> <a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
</td> </td>
</tr> </tr>
} }

View File

@@ -65,7 +65,7 @@
</div> </div>
<div class="span3"> <div class="span3">
@if(hasWritePermission){ @if(hasWritePermission){
<strong>Add Labels</strong> <span class="strong">Add Labels</span>
<div> <div>
<div id="label-list"> <div id="label-list">
<ul class="label-list nav nav-pills nav-stacked"> <ul class="label-list nav nav-pills nav-stacked">
@@ -111,7 +111,7 @@ $(function(){
if(milestoneId == ''){ if(milestoneId == ''){
$('#label-milestone').text('No milestone'); $('#label-milestone').text('No milestone');
} else { } else {
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<strong>').text(title))); $('#label-milestone').html($('<span>').append('Milestone: ').append($('<span class="strong">').text(title)));
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok'); $('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
} }
$('input[name=milestoneId]').val(milestoneId); $('input[name=milestoneId]').val(milestoneId);

View File

@@ -29,19 +29,19 @@
} }
<div class="small" style="text-align: center;"> <div class="small" style="text-align: center;">
@defining(comments.filter( _.action.contains("comment") ).size){ count => @defining(comments.filter( _.action.contains("comment") ).size){ count =>
<strong>@count</strong> @plural(count, "comment") <span class="strong">@count</span> @plural(count, "comment")
} }
</div> </div>
<hr/> <hr/>
<div style="margin-bottom: 8px;"> <div style="margin-bottom: 8px;">
<strong>Labels</strong> <span class="strong">Labels</span>
@if(hasWritePermission){ @if(hasWritePermission){
<div class="pull-right"> <div class="pull-right">
@helper.html.dropdown() { @helper.html.dropdown() {
@labels.map { label => @labels.map { label =>
<li> <li>
<a href="#" class="toggle-label" data-label-id="@label.labelId"> <a href="#" class="toggle-label" data-label-id="@label.labelId">
<i class="@{if(issueLabels.exists(_.labelId == label.labelId)) "icon-ok" else "icon-white"}"></i> @helper.html.checkicon(issueLabels.exists(_.labelId == label.labelId))
<span class="label" style="background-color: #@label.color;">&nbsp;</span> <span class="label" style="background-color: #@label.color;">&nbsp;</span>
@label.labelName @label.labelName
</a> </a>

View File

@@ -28,7 +28,11 @@
@helper.html.dropdown() { @helper.html.dropdown() {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li> <li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
@collaborators.map { collaborator => @collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li> <li>
<a href="javascript:void(0);" class="assign" data-name="@collaborator">
@helper.html.checkicon(Some(collaborator) == issue.assignedUserName)@avatar(collaborator, 20) @collaborator
</a>
</li>
} }
} }
} }
@@ -36,7 +40,7 @@
<span id="label-milestone"> <span id="label-milestone">
@issue.milestoneId.map { milestoneId => @issue.milestoneId.map { milestoneId =>
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) => @milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
Milestone: <strong>@milestone.title</strong> Milestone: <span class="strong">@milestone.title</span>
} }
}.getOrElse("No milestone") }.getOrElse("No milestone")
</span> </span>
@@ -53,7 +57,7 @@
@milestones.map { case (milestone, _, _) => @milestones.map { case (milestone, _, _) =>
<li> <li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title"> <a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
<i class="icon-white"></i> @milestone.title @helper.html.checkicon(Some(milestone.milestoneId) == issue.milestoneId) @milestone.title
<div class="small" style="padding-left: 20px;"> <div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate => @milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){ @if(isPast(dueDate)){
@@ -79,19 +83,12 @@
</div> </div>
<div class="issue-participants"> <div class="issue-participants">
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants => @defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
<strong>@participants.size</strong> @plural(participants.size, "participant") <span class="strong">@participants.size</span> @plural(participants.size, "participant")
@participants.map { participant => <a href="@url(participant)">@avatar(participant, 20, tooltip = true)</a> } @participants.map { participant => <a href="@url(participant)">@avatar(participant, 20, tooltip = true)</a> }
} }
</div> </div>
<script> <script>
$(function(){ $(function(){
@if(issue.assignedUserName.isDefined){
$('a.assign[data-name=@issue.assignedUserName] i').attr('class', 'icon-ok');
}
@if(issue.milestoneId.isDefined){
$('a.milestone[data-id=@issue.milestoneId] i').attr('class', 'icon-ok');
}
$('#edit').click(function(){ $('#edit').click(function(){
$.get('@url(repository)/issues/_data/@issue.issueId', $.get('@url(repository)/issues/_data/@issue.issueId',
{ {
@@ -138,7 +135,7 @@ $(function(){
$('#label-milestone').text('No milestone'); $('#label-milestone').text('No milestone');
$('#milestone-progress-area').empty(); $('#milestone-progress-area').empty();
} else { } else {
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<strong>').text(title))); $('#label-milestone').html($('<span>').append('Milestone: ').append($('<span class="strong">').text(title)));
$('#milestone-progress-area').html(data); $('#milestone-progress-area').html(data);
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok'); $('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
} }

View File

@@ -106,7 +106,7 @@
} }
} }
<hr/> <hr/>
<strong>Labels</strong> <span class="strong">Labels</span>
<div> <div>
<div id="label-list"> <div id="label-list">
<ul class="label-list nav nav-pills nav-stacked"> <ul class="label-list nav nav-pills nav-stacked">
@@ -127,7 +127,7 @@
<hr/> <hr/>
<input type="button" class="btn btn-block" id="manageLabel" data-toggle="button" value="Manage Labels"/> <input type="button" class="btn btn-block" id="manageLabel" data-toggle="button" value="Manage Labels"/>
<br/> <br/>
<strong>New label</strong> <span class="strong">New label</span>
@_root_.issues.labels.html.edit(None, repository) @_root_.issues.labels.html.edit(None, repository)
} }
</div> </div>

View File

@@ -29,52 +29,49 @@
<a class="btn@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a> <a class="btn@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a>
<a class="btn@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a> <a class="btn@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a>
</div> </div>
<div class="btn-group"> @helper.html.dropdown(
<button class="btn dropdown-toggle" data-toggle="dropdown"> value = (condition.sort, condition.direction) match {
Sort: case ("created" , "desc") => "Newest"
<strong> case ("created" , "asc" ) => "Oldest"
@if(condition.sort == "created" && condition.direction == "desc"){ Newest } case ("comments", "desc") => "Most commented"
@if(condition.sort == "created" && condition.direction == "asc" ){ Oldest } case ("comments", "asc" ) => "Least commented"
@if(condition.sort == "comments" && condition.direction == "desc"){ Most commented } case ("updated" , "desc") => "Recently updated"
@if(condition.sort == "comments" && condition.direction == "asc" ){ Least commented } case ("updated" , "asc" ) => "Least recently updated"
@if(condition.sort == "updated" && condition.direction == "desc"){ Recently updated } },
@if(condition.sort == "updated" && condition.direction == "asc" ){ Least recently updated } prefix = "Sort",
</strong> mini = false
<span class="caret"></span> ){
</button> <li>
<ul class="dropdown-menu"> <a href="@condition.copy(sort="created", direction="desc").toURL">
<li> @helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
<a href="@condition.copy(sort="created", direction="desc").toURL"> </a>
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest </li>
</a> <li>
</li> <a href="@condition.copy(sort="created", direction="asc" ).toURL">
<li> @helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
<a href="@condition.copy(sort="created", direction="asc" ).toURL"> </a>
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest </li>
</a> <li>
</li> <a href="@condition.copy(sort="comments", direction="desc").toURL">
<li> @helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
<a href="@condition.copy(sort="comments", direction="desc").toURL"> </a>
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented </li>
</a> <li>
</li> <a href="@condition.copy(sort="comments", direction="asc" ).toURL">
<li> @helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
<a href="@condition.copy(sort="comments", direction="asc" ).toURL"> </a>
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented </li>
</a> <li>
</li> <a href="@condition.copy(sort="updated", direction="desc").toURL">
<li> @helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
<a href="@condition.copy(sort="updated", direction="desc").toURL"> </a>
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated </li>
</a> <li>
</li> <a href="@condition.copy(sort="updated", direction="asc" ).toURL">
<li> @helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
<a href="@condition.copy(sort="updated", direction="asc" ).toURL"> </a>
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated </li>
</a> }
</li>
</ul>
</div>
<table class="table table-bordered table-hover table-issues"> <table class="table table-bordered table-hover table-issues">
@if(issues.isEmpty){ @if(issues.isEmpty){
<tr> <tr>
@@ -94,7 +91,7 @@
<tr> <tr>
<td style="background-color: #eee;"> <td style="background-color: #eee;">
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-mini" id="state"><strong>@{if(condition.state == "open") "Close" else "Reopen"}</strong></button> <button class="btn btn-mini strong" id="state">@{if(condition.state == "open") "Close" else "Reopen"}</button>
</div> </div>
@helper.html.dropdown("Label") { @helper.html.dropdown("Label") {
@labels.map { label => @labels.map { label =>

View File

@@ -11,12 +11,12 @@
<span id="error-title" class="error"></span> <span id="error-title" class="error"></span>
</fieldset> </fieldset>
<fieldset> <fieldset>
<label for="description"><strong>Description</strong></label> <label for="description" class="strong">Description</label>
<textarea id="description" name="description" style="width: 500px; height: 150px;">@milestone.map(_.description)</textarea> <textarea id="description" name="description" style="width: 500px; height: 150px;">@milestone.map(_.description)</textarea>
<span id="error-description" class="error"></span> <span id="error-description" class="error"></span>
</fieldset> </fieldset>
<fieldset> <fieldset>
<label for="dueDate"><strong>Due Date</strong></label> <label for="dueDate" class="strong">Due Date</label>
@helper.html.datepicker("dueDate", milestone.flatMap(_.dueDate)) @helper.html.datepicker("dueDate", milestone.flatMap(_.dueDate))
<span id="error-dueDate" class="error"></span> <span id="error-dueDate" class="error"></span>
</fieldset> </fieldset>

View File

@@ -5,10 +5,10 @@
<div style="width: 600px; margin: 10px auto;"> <div style="width: 600px; margin: 10px auto;">
<form id="form" method="post" action="@path/new" validate="true"> <form id="form" method="post" action="@path/new" validate="true">
<fieldset> <fieldset>
<label for="name"><strong>Repository name</strong></label> <label for="name" class="strong">Repository name</label>
<div class="btn-group" style="margin-bottom: 10px;" id="owner-dropdown"> <div class="btn-group" style="margin-bottom: 10px;" id="owner-dropdown">
<button class="btn dropdown-toggle" data-toggle="dropdown"> <button class="btn dropdown-toggle" data-toggle="dropdown">
<strong>@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</strong> <span class="strong">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</span>
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
@@ -24,13 +24,13 @@
<span id="error-name" class="error"></span> <span id="error-name" class="error"></span>
</fieldset> </fieldset>
<fieldset> <fieldset>
<label for="description"><strong>Description</strong> (optional)</label> <label for="description" class="strong">Description (optional)</label>
<input type="text" name="description" id="description" style="width: 95%;"/> <input type="text" name="description" id="description" style="width: 95%;"/>
</fieldset> </fieldset>
<fieldset class="margin"> <fieldset class="margin">
<label> <label>
<input type="radio" name="isPrivate" value="false" checked> <input type="radio" name="isPrivate" value="false" checked>
<strong>Public</strong><br> <span class="strong">Public</span><br>
<div> <div>
<span class="note">All users and guests can read this repository.</span> <span class="note">All users and guests can read this repository.</span>
</div> </div>
@@ -39,7 +39,7 @@
<fieldset> <fieldset>
<label> <label>
<input type="radio" name="isPrivate" value="true"> <input type="radio" name="isPrivate" value="true">
<strong>Private</strong><br> <span class="strong">Private</span><br>
<div> <div>
<span class="note">Only collaborators can read this repository.</span> <span class="note">Only collaborators can read this repository.</span>
</div> </div>
@@ -48,7 +48,7 @@
<fieldset class="margin"> <fieldset class="margin">
<label for="createReadme"> <label for="createReadme">
<input type="checkbox" name="createReadme" id="createReadme"/> <input type="checkbox" name="createReadme" id="createReadme"/>
<strong>Initialize this repository with a README</strong> <span class="strong">Initialize this repository with a README</span>
<div> <div>
<span class="note">This will allow you to <code>git clone</code> the repository immediately.</span> <span class="note">This will allow you to <code>git clone</code> the repository immediately.</span>
</div> </div>
@@ -68,6 +68,6 @@ $('#owner-dropdown a').click(function(){
$('#owner-dropdown i').attr('class', 'icon-white'); $('#owner-dropdown i').attr('class', 'icon-white');
$(this).find('i').attr('class', 'icon-ok'); $(this).find('i').attr('class', 'icon-ok');
$('#owner-dropdown strong').html($(this).find('span').html()); $('#owner-dropdown span.strong').html($(this).find('span').html());
}); });
</script> </script>

View File

@@ -1,7 +1,4 @@
@(issue: model.Issue, @(commits: Seq[Seq[util.JGitUtil.CommitInfo]],
pullreq: model.PullRequest,
commits: Seq[Seq[util.JGitUtil.CommitInfo]],
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._

View File

@@ -5,14 +5,12 @@
forkedId: String, forkedId: String,
sourceId: String, sourceId: String,
commitId: String, commitId: String,
hasConflict: Boolean,
repository: service.RepositoryService.RepositoryInfo, repository: service.RepositoryService.RepositoryInfo,
originRepository: service.RepositoryService.RepositoryInfo, originRepository: service.RepositoryService.RepositoryInfo,
forkedRepository: service.RepositoryService.RepositoryInfo, forkedRepository: service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: app.Context) hasWritePermission: Boolean)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
@html.main("Pull Requests - " + repository.owner + "/" + repository.name){ @html.main("Pull Requests - " + repository.owner + "/" + repository.name){
@html.header("pulls", repository) @html.header("pulls", repository)
<div class="pullreq-info"> <div class="pullreq-info">
@@ -53,14 +51,9 @@
<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/@repository.name/pulls/new" validate="true">
<div style="width: 260px; position: absolute; margin-left: 635px;"> <div style="width: 260px; position: absolute; margin-left: 635px;">
@if(hasConflict){ <div class="check-conflict" style="display: none;">
<h4>We cant automatically merge these branches</h4> <img src="@assets/common/images/indicator.gif"/> Checking...
<p>Don't worry, you can still submit the pull request.</p> </div>
} else {
<h4 style="color: #468847;">Able to merge</h4>
<p>These branches can be automatically merged.</p>
}
<input type="submit" class="btn btn-success btn-block" value="Send pull request"/>
</div> </div>
<div style="width: 620px; border-right: 1px solid #d4d4d4;"> <div style="width: 620px; border-right: 1px solid #d4d4d4;">
<span class="error" id="error-title"></span> <span class="error" id="error-title"></span>
@@ -82,59 +75,13 @@
<tr> <tr>
<td style="padding: 20px; background-color: #eee; text-align: center;"> <td style="padding: 20px; background-color: #eee; text-align: center;">
<h4>There isn't anything to compare.</h4> <h4>There isn't anything to compare.</h4>
<strong>@originRepository.owner:@originId</strong> and <strong>@forkedRepository.owner:@forkedId</strong> are identical. <span class="strong">@originRepository.owner:@originId</span> and <span class="strong">@forkedRepository.owner:@forkedId</span> are identical.
</td> </td>
</tr> </tr>
</table> </table>
} else { } else {
<div class="box"> @pulls.html.commits(commits, repository)
<table class="table table-file-list" style="border: 1px solid silver;"> @helper.html.diff(diffs, repository, Some(commitId), Some(sourceId), true)
@commits.map { day =>
<tr>
<th colspan="3" class="box-header" style="font-weight: normal;">@date(day.head.time)</th>
</tr>
@day.map { commit =>
<tr>
<td style="width: 20%;">
@avatar(commit.committer, 20)
<a href="@url(commit.committer)" class="username">@commit.committer</a>
</td>
<td>@commit.shortMessage</td>
<td style="width: 10%; text-align: right;">
<a href="@url(repository)/commit/@commit.id" class="monospace">@commit.id.substring(0, 7)</a>
</td>
</tr>
}
}
</table>
</div>
<div>
<div class="pull-right" style="margin-bottom: 10px;">
<input id="toggle-file-list" type="button" class="btn" value="Show file list"/>
</div>
Showing @diffs.size changed @plural(diffs.size, "file")
</div>
<ul id="commit-file-list" style="display: none;">
@diffs.zipWithIndex.map { case (diff, i) =>
<li@if(i > 0){ class="border"}>
<a href="#diff-@i">
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
<img src="@assets/common/images/diff_move.png"/> @diff.oldPath -> @diff.newPath
}
@if(diff.changeType == ChangeType.ADD){
<img src="@assets/common/images/diff_add.png"/> @diff.newPath
}
@if(diff.changeType == ChangeType.MODIFY){
<img src="@assets/common/images/diff_edit.png"/> @diff.newPath
}
@if(diff.changeType == ChangeType.DELETE){
<img src="@assets/common/images/diff_delete.png"/> @diff.oldPath
}
</a>
</li>
}
</ul>
@helper.html.diff(diffs, repository, Some(commitId), Some(sourceId))
} }
} }
<script> <script>
@@ -153,7 +100,7 @@ $(function(){
var e = $(this); var e = $(this);
e.parents('ul').find('i').attr('class', 'icon-white'); e.parents('ul').find('i').attr('class', 'icon-white');
e.find('i').attr('class', 'icon-ok'); e.find('i').attr('class', 'icon-ok');
e.parents('div.btn-group').find('button strong').text(e.text()); e.parents('div.btn-group').find('button span.strong').text(e.text());
@if(members.isEmpty){ @if(members.isEmpty){
location.href = '@url(repository)/compare/' + location.href = '@url(repository)/compare/' +
@@ -173,13 +120,26 @@ $(function(){
$('#pull-request-form').show(); $('#pull-request-form').show();
}); });
$('#toggle-file-list').click(function(){ @if(hasWritePermission){
$('#commit-file-list').toggle(); function checkConflict(from, to, noConflictHandler, hasConflictHandler){
if($(this).val() == 'Show file list'){ $('.check-conflict').show();
$(this).val('Hide file list'); $.get('@url(repository)/compare/' + from + '...' + to + '/mergecheck',
} else { function(data){ $('.check-conflict').html(data); });
$(this).val('Show file list');
} }
});
@if(members.isEmpty){
checkConflict(
$.trim($('i.icon-ok').parents('a.origin-branch').data('name')),
$.trim($('i.icon-ok').parents('a.forked-branch').data('name'))
);
} else {
checkConflict(
$.trim($('i.icon-ok').parents('a.origin-owner' ).data('name')) + ":" +
$.trim($('i.icon-ok').parents('a.origin-branch').data('name')),
$.trim($('i.icon-ok').parents('a.forked-owner' ).data('name')) + ":" +
$.trim($('i.icon-ok').parents('a.forked-branch').data('name'))
);
}
}
}); });
</script> </script>

View File

@@ -3,10 +3,8 @@
comments: List[model.IssueComment], comments: List[model.IssueComment],
collaborators: List[String], collaborators: List[String],
milestones: List[(model.Milestone, Int, Int)], milestones: List[(model.Milestone, Int, Int)],
hasConflict: Boolean,
hasWritePermission: Boolean, hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
requestRepositoryUrl: String)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
<div class="row-fluid"> <div class="row-fluid">
@@ -17,77 +15,14 @@
<div class="box issue-comment-box" style="background-color: #d8f5cd;"> <div class="box issue-comment-box" style="background-color: #d8f5cd;">
<div class="box-content"class="issue-content" style="border: 1px solid #95c97e; padding: 10px;"> <div class="box-content"class="issue-content" style="border: 1px solid #95c97e; padding: 10px;">
<div id="merge-pull-request"> <div id="merge-pull-request">
<div class="pull-right"> <div class="check-conflict" style="display: none;">
<input type="button" class="btn btn-success" id="merge-pull-request-button" value="Merge pull request"@if(hasConflict){ disabled="true"}/> <img src="@assets/common/images/indicator.gif"/> Checking...
</div>
<div>
@if(hasConflict){
<strong>We cant automatically merge this pull request.</strong>
} else {
<strong>This pull request can be automatically merged.</strong>
}
</div>
<div class="small">
@if(hasConflict){
<a href="#" id="show-command-line">Use the command line</a> to resolve conflicts before continuing.
} else {
You can also merge branches on the <a href="#" id="show-command-line">command line</a>.
}
</div>
<div id="command-line" style="display: none;">
<hr>
@if(hasConflict){
<strong>Checkout via command line</strong>
<p>
If you cannot merge a pull request automatically here, you have the option of checking
it out via command line to resolve conflicts and perform a manual merge.
</p>
} else {
<strong>Merging via command line</strong>
<p>
If you do not want to use the merge button or an automatic merge cannot be performed,
you can perform a manual merge on the command line.
</p>
}
@helper.html.copy("repository-url-copy", requestRepositoryUrl){
<input type="text" value="@requestRepositoryUrl" id="repository-url" readonly>
}
<div>
<p>
<strong>Step 1:</strong> Check out a new branch to test the changes — run this from your project directory
</p>
@defining(s"git checkout -b ${pullreq.requestUserName}-${pullreq.requestBranch} ${pullreq.requestBranch}"){ command =>
@helper.html.copy("merge-command-copy-1", command){
<pre style="width: 500px; float: left;">@command</pre>
}
}
</div>
<div>
<p>
<strong>Step 2:</strong> Bring in @{pullreq.requestUserName}'s changes and test
</p>
@defining(s"git pull ${requestRepositoryUrl} ${pullreq.requestBranch}"){ command =>
@helper.html.copy("merge-command-copy-2", command){
<pre style="width: 500px; float: left;">@command</pre>
}
}
</div>
<div>
<p>
<strong>Step 3:</strong> Merge the changes and update the server
</p>
@defining(s"git checkout master\ngit merge ${pullreq.requestUserName}-${pullreq.branch}\ngit push origin ${pullreq.branch}"){ command =>
@helper.html.copy("merge-command-copy-3", command){
<pre style="width: 500px; float: left;">@command</pre>
}
}
</div>
</div> </div>
</div> </div>
<div id="confirm-merge-form" style="display: none;"> <div id="confirm-merge-form" style="display: none;">
<form method="POST" action="@url(repository)/pull/@issue.issueId/merge"> <form method="POST" action="@url(repository)/pull/@issue.issueId/merge">
<div> <div class="strong">
<strong>Merge pull request #@issue.issueId from @{pullreq.requestUserName}/@{pullreq.requestBranch}</strong> Merge pull request #@issue.issueId from @{pullreq.requestUserName}/@{pullreq.requestBranch}
</div> </div>
<span id="error-message" class="error"></span> <span id="error-message" class="error"></span>
<textarea name="message" style="width: 680px; height: 80px;">@issue.title</textarea> <textarea name="message" style="width: 680px; height: 80px;">@issue.title</textarea>
@@ -113,26 +48,22 @@
<span class="label label-success issue-status">Open</span> <span class="label label-success issue-status">Open</span>
} }
<div class="small" style="text-align: center;"> <div class="small" style="text-align: center;">
<strong>@comments.size</strong> @plural(comments.size, "comment") <span class="strong">@comments.size</span> @plural(comments.size, "comment")
</div> </div>
<hr/> <hr/>
</div> </div>
</div> </div>
<script> <script>
$(function(){ $(function(){
$('#show-command-line').click(function(){
$('#command-line').show();
return false;
});
$('#merge-pull-request-button').click(function(){
$('#merge-pull-request').hide();
$('#confirm-merge-form').show();
});
$('#cancel-merge-pull-request').click(function(){ $('#cancel-merge-pull-request').click(function(){
$('#confirm-merge-form').hide(); $('#confirm-merge-form').hide();
$('#merge-pull-request').show(); $('#merge-pull-request').show();
}); });
@if(hasWritePermission){
$('.check-conflict').show();
$.get('@url(repository)/pull/@issue.issueId/mergeguide',
function(data){ $('.check-conflict').html(data); });
}
}); });
</script> </script>

View File

@@ -1,49 +0,0 @@
@(issue: model.Issue,
pullreq: model.PullRequest,
diffs: Seq[util.JGitUtil.DiffInfo],
newCommitId: String,
oldCommitId: String,
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
<div>
<div class="pull-right" style="margin-bottom: 10px;">
<input id="toggle-file-list" type="button" class="btn" value="Show file list"/>
</div>
Showing @diffs.size changed @plural(diffs.size, "file")
</div>
<ul id="commit-file-list" style="display: none;">
@diffs.zipWithIndex.map { case (diff, i) =>
<li@if(i > 0){ class="border"}>
<a href="#diff-@i">
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
<img src="@assets/common/images/diff_move.png"/> @diff.oldPath -> @diff.newPath
}
@if(diff.changeType == ChangeType.ADD){
<img src="@assets/common/images/diff_add.png"/> @diff.newPath
}
@if(diff.changeType == ChangeType.MODIFY){
<img src="@assets/common/images/diff_edit.png"/> @diff.newPath
}
@if(diff.changeType == ChangeType.DELETE){
<img src="@assets/common/images/diff_delete.png"/> @diff.oldPath
}
</a>
</li>
}
</ul>
@helper.html.diff(diffs, repository, Some(newCommitId), Some(oldCommitId))
<script>
$(function(){
$('#toggle-file-list').click(function(){
$('#commit-file-list').toggle();
if($(this).val() == 'Show file list'){
$(this).val('Hide file list');
} else {
$(this).val('Show file list');
}
});
});
</script>

View File

@@ -20,52 +20,49 @@
<a class="btn@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a> <a class="btn@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a>
<a class="btn@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a> <a class="btn@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a>
</div> </div>
<div class="btn-group"> @helper.html.dropdown(
<button class="btn dropdown-toggle" data-toggle="dropdown"> value = (condition.sort, condition.direction) match {
Sort: case ("created" , "desc") => "Newest"
<strong> case ("created" , "asc" ) => "Oldest"
@if(condition.sort == "created" && condition.direction == "desc"){ Newest } case ("comments", "desc") => "Most commented"
@if(condition.sort == "created" && condition.direction == "asc" ){ Oldest } case ("comments", "asc" ) => "Least commented"
@if(condition.sort == "comments" && condition.direction == "desc"){ Most commented } case ("updated" , "desc") => "Recently updated"
@if(condition.sort == "comments" && condition.direction == "asc" ){ Least commented } case ("updated" , "asc" ) => "Least recently updated"
@if(condition.sort == "updated" && condition.direction == "desc"){ Recently updated } },
@if(condition.sort == "updated" && condition.direction == "asc" ){ Least recently updated } prefix = "Sort",
</strong> mini = false
<span class="caret"></span> ){
</button> <li>
<ul class="dropdown-menu"> <a href="@condition.copy(sort="created", direction="desc").toURL">
<li> @helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
<a href="@condition.copy(sort="created", direction="desc").toURL"> </a>
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest </li>
</a> <li>
</li> <a href="@condition.copy(sort="created", direction="asc" ).toURL">
<li> @helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
<a href="@condition.copy(sort="created", direction="asc" ).toURL"> </a>
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest </li>
</a> <li>
</li> <a href="@condition.copy(sort="comments", direction="desc").toURL">
<li> @helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
<a href="@condition.copy(sort="comments", direction="desc").toURL"> </a>
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented </li>
</a> <li>
</li> <a href="@condition.copy(sort="comments", direction="asc" ).toURL">
<li> @helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
<a href="@condition.copy(sort="comments", direction="asc" ).toURL"> </a>
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented </li>
</a> <li>
</li> <a href="@condition.copy(sort="updated", direction="desc").toURL">
<li> @helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
<a href="@condition.copy(sort="updated", direction="desc").toURL"> </a>
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated </li>
</a> <li>
</li> <a href="@condition.copy(sort="updated", direction="asc" ).toURL">
<li> @helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
<a href="@condition.copy(sort="updated", direction="asc" ).toURL"> </a>
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated </li>
</a> }
</li>
</ul>
</div>
<table class="table table-bordered table-hover table-issues"> <table class="table table-bordered table-hover table-issues">
@if(issues.isEmpty){ @if(issues.isEmpty){
<tr> <tr>

View File

@@ -0,0 +1,9 @@
@(hasConflict: Boolean)
@if(hasConflict){
<h4>We cant automatically merge these branches</h4>
<p>Don't worry, you can still submit the pull request.</p>
} else {
<h4 style="color: #468847;">Able to merge</h4>
<p>These branches can be automatically merged.</p>
}
<input type="submit" class="btn btn-success btn-block" value="Send pull request"/>

View File

@@ -0,0 +1,84 @@
@(hasConflict: Boolean,
pullreq: model.PullRequest,
requestRepositoryUrl: String)(implicit context: app.Context)
@import context._
@import view.helpers._
<div class="pull-right">
<input type="button" class="btn btn-success" id="merge-pull-request-button" value="Merge pull request"@if(hasConflict){ disabled="true"}/>
</div>
<div>
@if(hasConflict){
<span class="strong">We cant automatically merge this pull request.</span>
} else {
<span class="strong">This pull request can be automatically merged.</span>
}
</div>
<div class="small">
@if(hasConflict){
<a href="#" id="show-command-line">Use the command line</a> to resolve conflicts before continuing.
} else {
You can also merge branches on the <a href="#" id="show-command-line">command line</a>.
}
</div>
<div id="command-line" style="display: none;">
<hr>
@if(hasConflict){
<span class="strong">Checkout via command line</span>
<p>
If you cannot merge a pull request automatically here, you have the option of checking
it out via command line to resolve conflicts and perform a manual merge.
</p>
} else {
<span class="strong">Merging via command line</span>
<p>
If you do not want to use the merge button or an automatic merge cannot be performed,
you can perform a manual merge on the command line.
</p>
}
@helper.html.copy("repository-url-copy", requestRepositoryUrl){
<input type="text" value="@requestRepositoryUrl" id="repository-url" readonly>
}
<div>
<p>
<span class="strong">Step 1:</span> Check out a new branch to test the changes — run this from your project directory
</p>
@defining(s"git checkout -b ${pullreq.requestUserName}-${pullreq.requestBranch} ${pullreq.requestBranch}"){ command =>
@helper.html.copy("merge-command-copy-1", command){
<pre style="width: 500px; float: left;">@command</pre>
}
}
</div>
<div>
<p>
<span class="strong">Step 2:</span> Bring in @{pullreq.requestUserName}'s changes and test
</p>
@defining(s"git pull ${requestRepositoryUrl} ${pullreq.requestBranch}"){ command =>
@helper.html.copy("merge-command-copy-2", command){
<pre style="width: 500px; float: left;">@command</pre>
}
}
</div>
<div>
<p>
<span class="strong">Step 3:</span> Merge the changes and update the server
</p>
@defining(s"git checkout master\ngit merge ${pullreq.requestUserName}-${pullreq.branch}\ngit push origin ${pullreq.branch}"){ command =>
@helper.html.copy("merge-command-copy-3", command){
<pre style="width: 500px; float: left;">@command</pre>
}
}
</div>
</div>
<script>
$(function(){
$('#show-command-line').click(function(){
$('#command-line').show();
return false;
});
$('#merge-pull-request-button').click(function(){
$('#merge-pull-request').hide();
$('#confirm-merge-form').show();
});
});
</script>

View File

@@ -5,10 +5,8 @@
milestones: List[(model.Milestone, Int, Int)], milestones: List[(model.Milestone, Int, Int)],
dayByDayCommits: Seq[Seq[util.JGitUtil.CommitInfo]], dayByDayCommits: Seq[Seq[util.JGitUtil.CommitInfo]],
diffs: Seq[util.JGitUtil.DiffInfo], diffs: Seq[util.JGitUtil.DiffInfo],
hasConflict: Boolean,
hasWritePermission: Boolean, hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
requestRepositoryUrl: String)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main("%s - Pull Request #%d - %s/%s".format(issue.title, issue.issueId, repository.owner, repository.name)){ @html.main("%s - Pull Request #%d - %s/%s".format(issue.title, issue.issueId, repository.owner, repository.name)){
@@ -39,13 +37,13 @@
</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, hasConflict, hasWritePermission, repository, requestRepositoryUrl) @pulls.html.discussion(issue, pullreq, comments, collaborators, milestones, hasWritePermission, repository)
</div> </div>
<div class="tab-pane" id="commits"> <div class="tab-pane" id="commits">
@pulls.html.commits(issue, pullreq, dayByDayCommits, hasWritePermission, repository) @pulls.html.commits(dayByDayCommits, repository)
</div> </div>
<div class="tab-pane" id="files"> <div class="tab-pane" id="files">
@pulls.html.files(issue, pullreq, diffs, commits.head.id, commits.last.id, hasWritePermission, repository) @helper.html.diff(diffs, repository, Some(commits.head.id), Some(commits.last.id), true)
</div> </div>
</div> </div>
} }

View File

@@ -8,7 +8,6 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@import util.Implicits._ @import util.Implicits._
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
@html.main(commit.shortMessage, Some(repository)){ @html.main(commit.shortMessage, Some(repository)){
@html.header("code", repository) @html.header("code", repository)
@tab(commitId, repository, "commits") @tab(commitId, repository, "commits")
@@ -71,52 +70,10 @@
</td> </td>
</tr> </tr>
</table> </table>
<div> @helper.html.diff(diffs, repository, Some(commit.id), oldCommitId, true)
<div class="pull-right" style="margin-bottom: 10px;">
<input id="toggle-file-list" type="button" class="btn" value="Show file list"/>
</div>
Showing @diffs.size changed @plural(diffs.size, "file")
@*
@if(diffs.size == 1){
Showing 1 changed file
} else {
Showing @diffs.size changed files
}
*@
</div>
<ul id="commit-file-list" style="display: none;">
@diffs.zipWithIndex.map { case (diff, i) =>
<li@if(i > 0){ class="border"}>
<a href="#diff-@i">
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
<img src="@assets/common/images/diff_move.png"/> @diff.oldPath -> @diff.newPath
}
@if(diff.changeType == ChangeType.ADD){
<img src="@assets/common/images/diff_add.png"/> @diff.newPath
}
@if(diff.changeType == ChangeType.MODIFY){
<img src="@assets/common/images/diff_edit.png"/> @diff.newPath
}
@if(diff.changeType == ChangeType.DELETE){
<img src="@assets/common/images/diff_delete.png"/> @diff.oldPath
}
</a>
</li>
}
</ul>
@helper.html.diff(diffs, repository, Some(commit.id), oldCommitId)
} }
<script> <script>
$(function(){ $(function(){
$('#toggle-file-list').click(function(){
$('#commit-file-list').toggle();
if($(this).val() == 'Show file list'){
$(this).val('Hide file list');
} else {
$(this).val('Show file list');
}
});
$('a.branch:first, a.tag:first').css({ $('a.branch:first, a.tag:first').css({
'font-weight': 'bold', 'font-weight': 'bold',
'color': '#555555' 'color': '#555555'

View File

@@ -5,25 +5,16 @@
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
@if(!hideBranchPulldown){ @if(!hideBranchPulldown){
<li> <li>
<div class="btn-group" style="margin-right: 20px;"> @helper.html.dropdown(
<button class="btn dropdown-toggle" data-toggle="dropdown"> value = if(id.length == 40) id.substring(0, 10) else id,
@if(id.length == 40){ prefix = if(id.length == 40) "tree" else if(repository.branchList.contains(id)) "branch" else "tree",
tree: <strong>@id.substring(0, 10)</strong> mini = false,
} style = "margin-right: 20px;"
@if(repository.branchList.contains(id)){ ){
branch: <strong>@id</strong> @repository.branchList.map { branch =>
} <li><a href="@url(repository)/@if(active=="commits"){commits} else {tree}/@branch">@helper.html.checkicon(id == branch) @branch</a></li>
@if(repository.tags.exists(_.name == id)){ }
tag: <strong>@id</strong> }
}
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
@repository.branchList.map { branch =>
<li><a href="@url(repository)/@if(active=="commits"){commits} else {tree}/@branch">@helper.html.checkicon(id == branch) @branch</a></li>
}
</ul>
</div>
</li> </li>
} }
<li@if(active=="files" ){ class="active"}><a href="@url(repository)/tree/@id">Files</a></li> <li@if(active=="files" ){ class="active"}><a href="@url(repository)/tree/@id">Files</a></li>

View File

@@ -24,7 +24,7 @@
Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a> Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a>
at @datetime(issue.registeredDate) at @datetime(issue.registeredDate)
@if(issue.commentCount > 0){ @if(issue.commentCount > 0){
&nbsp;&nbsp;<i class="icon-comment"></i><strong>@issue.commentCount</strong> @plural(issue.commentCount, "comment") &nbsp;&nbsp;<i class="icon-comment"></i><span class="strong">@issue.commentCount</span> @plural(issue.commentCount, "comment")
} }
</div> </div>
</div> </div>

View File

@@ -22,21 +22,9 @@
<div> <div>
<span class="error" id="error-userName"></span> <span class="error" id="error-userName"></span>
</div> </div>
<input type="text" name="userName" id="userName" style="width: 300px; margin-bottom: 0px;"/> @helper.html.account("userName", 300)
<input type="submit" class="btn" value="Add"/> <input type="submit" class="btn" value="Add"/>
</form> </form>
} }
} }
} }
<script>
$(function(){
$('#userName').typeahead({
source: function (query, process) {
return $.get('@path/_user/proposals', { query: query },
function (data) {
return process(data.options);
});
}
});
});
</script>

View File

@@ -0,0 +1,23 @@
@(webHooks: List[model.WebHook], repository: service.RepositoryService.RepositoryInfo, info: Option[Any])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Settings", Some(repository)){
@html.header("settings", repository)
@menu("hooks", repository){
@helper.html.information(info)
<h3>WebHook URLs</h3>
<ul>
@webHooks.map { webHook =>
<li>@webHook.url <a href="@url(repository)/settings/hooks/delete?url=@urlEncode(webHook.url)" class="remove">(remove)</a></li>
}
</ul>
<form method="POST" action="@url(repository)/settings/hooks/add" validate="true">
<div>
<span class="error" id="error-url"></span>
</div>
<input type="text" name="url" id="url" style="width: 300px; margin-bottom: 0px;"/>
<input type="submit" class="btn" value="Add"/>
<a href="@url(repository)/settings/hooks/test" class="btn">Test Hook</a>
</form>
}
}

View File

@@ -11,6 +11,9 @@
<li@if(active=="collaborators"){ class="active"}> <li@if(active=="collaborators"){ class="active"}>
<a href="@url(repository)/settings/collaborators">Collaborators</a> <a href="@url(repository)/settings/collaborators">Collaborators</a>
</li> </li>
<li@if(active=="hooks"){ class="active"}>
<a href="@url(repository)/settings/hooks">Service Hooks</a>
</li>
<li@if(active=="delete"){ class="active"}> <li@if(active=="delete"){ class="active"}>
<a href="@url(repository)/settings/delete">Delete Repository</a> <a href="@url(repository)/settings/delete">Delete Repository</a>
</li> </li>

View File

@@ -10,11 +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="description"><strong>Description</strong></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>
<fieldset class="margin"> <fieldset class="margin">
<label for="defaultBranch"><strong>Default Branch</strong></label> <label for="defaultBranch" class="strong">Default Branch</label>
<select name="defaultBranch" id="defaultBranch"> <select name="defaultBranch" id="defaultBranch">
@repository.branchList.map { branch => @repository.branchList.map { branch =>
<option value="@branch"@if(branch==repository.repository.defaultBranch){ selected}>@branch</option> <option value="@branch"@if(branch==repository.repository.defaultBranch){ selected}>@branch</option>
@@ -28,7 +28,7 @@
@if(!repository.repository.isPrivate ){ checked } @if(!repository.repository.isPrivate ){ checked }
@if(repository.repository.parentUserName.isDefined){ disabled } @if(repository.repository.parentUserName.isDefined){ disabled }
> >
<strong>Public</strong><br> <span class="strong">Public</span><br>
<div> <div>
<span class="note">All users and guests can read this repository.</span> <span class="note">All users and guests can read this repository.</span>
</div> </div>
@@ -40,7 +40,7 @@
@if(repository.repository.isPrivate ){ checked } @if(repository.repository.isPrivate ){ checked }
@if(repository.repository.parentUserName.isDefined){ disabled } @if(repository.repository.parentUserName.isDefined){ disabled }
> >
<strong>Private</strong><br> <span class="strong">Private</span><br>
<div> <div>
<span class="note">Only collaborators can read this repository.</span> <span class="note">Only collaborators can read this repository.</span>
</div> </div>
@@ -54,8 +54,8 @@
<div class="box-content"> <div class="box-content">
<dl> <dl>
<dt> <dt>
<label class="checkbox"> <label class="checkbox strong">
<input type="checkbox" name="wiki" id="wiki"/> <strong>Wiki</strong> <input type="checkbox" name="wiki" id="wiki"/> Wiki
</label> </label>
</dt> </dt>
<dd> <dd>
@@ -67,8 +67,8 @@
<hr> <hr>
<dl> <dl>
<dt> <dt>
<label class="checkbox"> <label class="checkbox strong">
<input type="checkbox" name="issue" id="issue"/> <strong>Issue</strong> <input type="checkbox" name="issue" id="issue"/> Issue
</label> </label>
</dt> </dt>
<dd> <dd>

View File

@@ -22,5 +22,5 @@
</div> </div>
</li> </li>
</ul> </ul>
@helper.html.diff(diffs, repository, None, None) @helper.html.diff(diffs, repository, None, None, false)
} }

View File

@@ -2,8 +2,8 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li@if(active == "home"){ class="active"}><a href="@url(repository)/wiki">Home</a></li> <li@if(active == "home" ){ class="active"}><a href="@url(repository)/wiki">Home</a></li>
<li@if(active == "pages"){ class="active"}><a href="@url(repository)/wiki/_pages">Pages</a></li> <li@if(active == "pages" ){ class="active"}><a href="@url(repository)/wiki/_pages">Pages</a></li>
<li@if(active == "history"){ class="active"}><a href="@url(repository)/wiki/_history">Wiki History</a></li> <li@if(active == "history"){ class="active"}><a href="@url(repository)/wiki/_history">Wiki History</a></li>
<li class="pull-right"> <li class="pull-right">
@defining(repository.url.replaceFirst("\\.git$", ".wiki.git")){ repositoryUrl => @defining(repository.url.replaceFirst("\\.git$", ".wiki.git")){ repositoryUrl =>