Merge branch 'master' into slick2

Conflicts:
	project/build.scala
	src/main/scala/app/IndexController.scala
	src/main/scala/app/RepositorySettingsController.scala
	src/main/scala/model/Account.scala
	src/main/scala/model/BasicTemplate.scala
	src/main/scala/model/Issue.scala
	src/main/scala/model/IssueComment.scala
	src/main/scala/model/package.scala
	src/main/scala/service/IssuesService.scala
	src/main/scala/service/PullRequestService.scala
	src/main/scala/service/RepositoryService.scala
	src/main/scala/service/WikiService.scala
	src/main/scala/servlet/TransactionFilter.scala
	src/main/scala/util/Notifier.scala
This commit is contained in:
Naoki Takezoe
2014-07-06 17:02:49 +09:00
287 changed files with 98327 additions and 61369 deletions

View File

@@ -80,6 +80,11 @@ Run the following commands in `Terminal` to
Release Notes Release Notes
-------- --------
### 2.1 - xx Sep 2014
- Upgrade to Slick 2.0 from 1.9
- Base part of the plug-in system is merged
- Many bug fix and improvements
### 2.0 - 31 May 2014 ### 2.0 - 31 May 2014
- Modern Github UI - Modern Github UI
- Preview in AceEditor - Preview in AceEditor

View File

@@ -50,8 +50,8 @@
</target> </target>
<target name="rename" depends="embed"> <target name="rename" depends="embed">
<rename src="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war" <move file="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
dest="${target.dir}/scala-${scala.version}/gitbucket.war"/> tofile="${target.dir}/scala-${scala.version}/gitbucket.war"/>
</target> </target>
<target name="all" depends="rename"> <target name="all" depends="rename">

View File

@@ -39,7 +39,9 @@ object MyBuild extends Build {
"org.apache.httpcomponents" % "httpclient" % "4.3", "org.apache.httpcomponents" % "httpclient" % "4.3",
"org.apache.sshd" % "apache-sshd" % "0.11.0", "org.apache.sshd" % "apache-sshd" % "0.11.0",
"com.typesafe.slick" %% "slick" % "2.0.2", "com.typesafe.slick" %% "slick" % "2.0.2",
"org.mozilla" % "rhino" % "1.7R4",
"com.novell.ldap" % "jldap" % "2009-10-07", "com.novell.ldap" % "jldap" % "2009-10-07",
"org.quartz-scheduler" % "quartz" % "2.2.1",
"com.h2database" % "h2" % "1.3.173", "com.h2database" % "h2" % "1.3.173",
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime", "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided", "org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",

View File

@@ -1,135 +1,135 @@
CREATE TABLE ACCOUNT( CREATE TABLE ACCOUNT(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
MAIL_ADDRESS VARCHAR(100) NOT NULL, MAIL_ADDRESS VARCHAR(100) NOT NULL,
PASSWORD VARCHAR(40) NOT NULL, PASSWORD VARCHAR(40) NOT NULL,
ADMINISTRATOR BOOLEAN NOT NULL, ADMINISTRATOR BOOLEAN NOT NULL,
URL VARCHAR(200), URL VARCHAR(200),
REGISTERED_DATE TIMESTAMP NOT NULL, REGISTERED_DATE TIMESTAMP NOT NULL,
UPDATED_DATE TIMESTAMP NOT NULL, UPDATED_DATE TIMESTAMP NOT NULL,
LAST_LOGIN_DATE TIMESTAMP LAST_LOGIN_DATE TIMESTAMP
); );
CREATE TABLE REPOSITORY( CREATE TABLE REPOSITORY(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
PRIVATE BOOLEAN NOT NULL, PRIVATE BOOLEAN NOT NULL,
DESCRIPTION TEXT, DESCRIPTION TEXT,
DEFAULT_BRANCH VARCHAR(100), DEFAULT_BRANCH VARCHAR(100),
REGISTERED_DATE TIMESTAMP NOT NULL, REGISTERED_DATE TIMESTAMP NOT NULL,
UPDATED_DATE TIMESTAMP NOT NULL, UPDATED_DATE TIMESTAMP NOT NULL,
LAST_ACTIVITY_DATE TIMESTAMP NOT NULL LAST_ACTIVITY_DATE TIMESTAMP NOT NULL
); );
CREATE TABLE COLLABORATOR( CREATE TABLE COLLABORATOR(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
COLLABORATOR_NAME VARCHAR(100) NOT NULL COLLABORATOR_NAME VARCHAR(100) NOT NULL
); );
CREATE TABLE ISSUE( CREATE TABLE ISSUE(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
ISSUE_ID INT NOT NULL, ISSUE_ID INT NOT NULL,
OPENED_USER_NAME VARCHAR(100) NOT NULL, OPENED_USER_NAME VARCHAR(100) NOT NULL,
MILESTONE_ID INT, MILESTONE_ID INT,
ASSIGNED_USER_NAME VARCHAR(100), ASSIGNED_USER_NAME VARCHAR(100),
TITLE TEXT NOT NULL, TITLE TEXT NOT NULL,
CONTENT TEXT, CONTENT TEXT,
CLOSED BOOLEAN NOT NULL, CLOSED BOOLEAN NOT NULL,
REGISTERED_DATE TIMESTAMP NOT NULL, REGISTERED_DATE TIMESTAMP NOT NULL,
UPDATED_DATE TIMESTAMP NOT NULL UPDATED_DATE TIMESTAMP NOT NULL
); );
CREATE TABLE ISSUE_ID( CREATE TABLE ISSUE_ID(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
ISSUE_ID INT NOT NULL ISSUE_ID INT NOT NULL
); );
CREATE TABLE ISSUE_COMMENT( CREATE TABLE ISSUE_COMMENT(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
ISSUE_ID INT NOT NULL, ISSUE_ID INT NOT NULL,
COMMENT_ID INT AUTO_INCREMENT, COMMENT_ID INT AUTO_INCREMENT,
ACTION VARCHAR(10), ACTION VARCHAR(10),
COMMENTED_USER_NAME VARCHAR(100) NOT NULL, COMMENTED_USER_NAME VARCHAR(100) NOT NULL,
CONTENT TEXT NOT NULL, CONTENT TEXT NOT NULL,
REGISTERED_DATE TIMESTAMP NOT NULL, REGISTERED_DATE TIMESTAMP NOT NULL,
UPDATED_DATE TIMESTAMP NOT NULL UPDATED_DATE TIMESTAMP NOT NULL
); );
CREATE TABLE LABEL( CREATE TABLE LABEL(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
LABEL_ID INT AUTO_INCREMENT, LABEL_ID INT AUTO_INCREMENT,
LABEL_NAME VARCHAR(100) NOT NULL, LABEL_NAME VARCHAR(100) NOT NULL,
COLOR CHAR(6) NOT NULL COLOR CHAR(6) NOT NULL
); );
CREATE TABLE ISSUE_LABEL( CREATE TABLE ISSUE_LABEL(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
ISSUE_ID INT NOT NULL, ISSUE_ID INT NOT NULL,
LABEL_ID INT NOT NULL LABEL_ID INT NOT NULL
); );
CREATE TABLE MILESTONE( CREATE TABLE MILESTONE(
USER_NAME VARCHAR(100) NOT NULL, USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL, REPOSITORY_NAME VARCHAR(100) NOT NULL,
MILESTONE_ID INT AUTO_INCREMENT, MILESTONE_ID INT AUTO_INCREMENT,
TITLE VARCHAR(100) NOT NULL, TITLE VARCHAR(100) NOT NULL,
DESCRIPTION TEXT, DESCRIPTION TEXT,
DUE_DATE TIMESTAMP, DUE_DATE TIMESTAMP,
CLOSED_DATE TIMESTAMP CLOSED_DATE TIMESTAMP
); );
ALTER TABLE ACCOUNT ADD CONSTRAINT IDX_ACCOUNT_PK PRIMARY KEY (USER_NAME); ALTER TABLE ACCOUNT ADD CONSTRAINT IDX_ACCOUNT_PK PRIMARY KEY (USER_NAME);
ALTER TABLE ACCOUNT ADD CONSTRAINT IDX_ACCOUNT_1 UNIQUE (MAIL_ADDRESS); ALTER TABLE ACCOUNT ADD CONSTRAINT IDX_ACCOUNT_1 UNIQUE (MAIL_ADDRESS);
ALTER TABLE REPOSITORY ADD CONSTRAINT IDX_REPOSITORY_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME); ALTER TABLE REPOSITORY ADD CONSTRAINT IDX_REPOSITORY_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE REPOSITORY ADD CONSTRAINT IDX_REPOSITORY_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME); ALTER TABLE REPOSITORY ADD CONSTRAINT IDX_REPOSITORY_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME); ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME); ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK1 FOREIGN KEY (COLLABORATOR_NAME) REFERENCES ACCOUNT (USER_NAME); ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK1 FOREIGN KEY (COLLABORATOR_NAME) REFERENCES ACCOUNT (USER_NAME);
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_PK PRIMARY KEY (ISSUE_ID, USER_NAME, REPOSITORY_NAME); ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_PK PRIMARY KEY (ISSUE_ID, USER_NAME, REPOSITORY_NAME);
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME); ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK1 FOREIGN KEY (OPENED_USER_NAME) REFERENCES ACCOUNT (USER_NAME); ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK1 FOREIGN KEY (OPENED_USER_NAME) REFERENCES ACCOUNT (USER_NAME);
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK2 FOREIGN KEY (MILESTONE_ID) REFERENCES MILESTONE (MILESTONE_ID); ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK2 FOREIGN KEY (MILESTONE_ID) REFERENCES MILESTONE (MILESTONE_ID);
ALTER TABLE ISSUE_ID ADD CONSTRAINT IDX_ISSUE_ID_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME); ALTER TABLE ISSUE_ID ADD CONSTRAINT IDX_ISSUE_ID_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE ISSUE_ID ADD CONSTRAINT IDX_ISSUE_ID_FK1 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME); ALTER TABLE ISSUE_ID ADD CONSTRAINT IDX_ISSUE_ID_FK1 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_PK PRIMARY KEY (COMMENT_ID); ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_PK PRIMARY KEY (COMMENT_ID);
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_1 UNIQUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID, COMMENT_ID); ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_1 UNIQUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID, COMMENT_ID);
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID); ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
ALTER TABLE LABEL ADD CONSTRAINT IDX_LABEL_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, LABEL_ID); ALTER TABLE LABEL ADD CONSTRAINT IDX_LABEL_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, LABEL_ID);
ALTER TABLE LABEL ADD CONSTRAINT IDX_LABEL_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME); ALTER TABLE LABEL ADD CONSTRAINT IDX_LABEL_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE ISSUE_LABEL ADD CONSTRAINT IDX_ISSUE_LABEL_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID, LABEL_ID); ALTER TABLE ISSUE_LABEL ADD CONSTRAINT IDX_ISSUE_LABEL_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID, LABEL_ID);
ALTER TABLE ISSUE_LABEL ADD CONSTRAINT IDX_ISSUE_LABEL_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID); ALTER TABLE ISSUE_LABEL ADD CONSTRAINT IDX_ISSUE_LABEL_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
ALTER TABLE MILESTONE ADD CONSTRAINT IDX_MILESTONE_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, MILESTONE_ID); ALTER TABLE MILESTONE ADD CONSTRAINT IDX_MILESTONE_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, MILESTONE_ID);
ALTER TABLE MILESTONE ADD CONSTRAINT IDX_MILESTONE_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME); ALTER TABLE MILESTONE ADD CONSTRAINT IDX_MILESTONE_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
INSERT INTO ACCOUNT ( INSERT INTO ACCOUNT (
USER_NAME, USER_NAME,
MAIL_ADDRESS, MAIL_ADDRESS,
PASSWORD, PASSWORD,
ADMINISTRATOR, ADMINISTRATOR,
URL, URL,
REGISTERED_DATE, REGISTERED_DATE,
UPDATED_DATE, UPDATED_DATE,
LAST_LOGIN_DATE LAST_LOGIN_DATE
) VALUES ( ) VALUES (
'root', 'root',
'root@localhost', 'root@localhost',
'dc76e9f0c0006e8f919e0c515c66dbba3982f785', 'dc76e9f0c0006e8f919e0c515c66dbba3982f785',
true, true,
'https://github.com/takezoe/gitbucket', 'https://github.com/takezoe/gitbucket',
SYSDATE, SYSDATE,
SYSDATE, SYSDATE,
NULL NULL
); );

View File

@@ -1,4 +1,4 @@
import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter} import _root_.servlet.{PluginActionInvokeFilter, BasicAuthenticationFilter, TransactionFilter}
import app._ import app._
//import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider //import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
import org.scalatra._ import org.scalatra._
@@ -10,6 +10,8 @@ class ScalatraBootstrap extends LifeCycle {
// Register TransactionFilter and BasicAuthenticationFilter at first // Register TransactionFilter and BasicAuthenticationFilter at first
context.addFilter("transactionFilter", new TransactionFilter) context.addFilter("transactionFilter", new TransactionFilter)
context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*") context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
context.addFilter("pluginActionInvokeFilter", new PluginActionInvokeFilter)
context.getFilterRegistration("pluginActionInvokeFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter) context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter)
context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*") context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")

View File

@@ -292,7 +292,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
* Create new repository. * Create new repository.
*/ */
post("/new", newRepositoryForm)(usersOnly { form => post("/new", newRepositoryForm)(usersOnly { form =>
LockUtil.lock(s"${form.owner}/${form.name}/create"){ LockUtil.lock(s"${form.owner}/${form.name}"){
if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){ if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){
val ownerAccount = getAccountByUserName(form.owner).get val ownerAccount = getAccountByUserName(form.owner).get
val loginAccount = context.loginAccount.get val loginAccount = context.loginAccount.get
@@ -355,7 +355,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val loginAccount = context.loginAccount.get val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName val loginUserName = loginAccount.userName
LockUtil.lock(s"${loginUserName}/${repository.name}/create"){ LockUtil.lock(s"${loginUserName}/${repository.name}"){
if(repository.owner == loginUserName || getRepository(loginAccount.userName, repository.name, baseUrl).isDefined){ if(repository.owner == loginUserName || getRepository(loginAccount.userName, repository.name, baseUrl).isDefined){
// redirect to the repository if repository already exists // redirect to the repository if repository already exists
redirect(s"/${loginUserName}/${repository.name}") redirect(s"/${loginUserName}/${repository.name}")

View File

@@ -1,109 +1,109 @@
package app package app
import service._ import service._
import util.{UsersAuthenticator, Keys} import util.{UsersAuthenticator, Keys}
import util.Implicits._ 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
with UsersAuthenticator with UsersAuthenticator
trait DashboardControllerBase extends ControllerBase { trait DashboardControllerBase extends ControllerBase {
self: IssuesService with PullRequestService with RepositoryService with UsersAuthenticator => self: IssuesService with PullRequestService with RepositoryService with UsersAuthenticator =>
get("/dashboard/issues/repos")(usersOnly { get("/dashboard/issues/repos")(usersOnly {
searchIssues("all") searchIssues("all")
}) })
get("/dashboard/issues/assigned")(usersOnly { get("/dashboard/issues/assigned")(usersOnly {
searchIssues("assigned") searchIssues("assigned")
}) })
get("/dashboard/issues/created_by")(usersOnly { get("/dashboard/issues/created_by")(usersOnly {
searchIssues("created_by") searchIssues("created_by")
}) })
get("/dashboard/pulls")(usersOnly { get("/dashboard/pulls")(usersOnly {
searchPullRequests("created_by", None) searchPullRequests("created_by", None)
}) })
get("/dashboard/pulls/owned")(usersOnly { get("/dashboard/pulls/owned")(usersOnly {
searchPullRequests("created_by", None) searchPullRequests("created_by", None)
}) })
get("/dashboard/pulls/public")(usersOnly { get("/dashboard/pulls/public")(usersOnly {
searchPullRequests("not_created_by", None) searchPullRequests("not_created_by", None)
}) })
get("/dashboard/pulls/for/:owner/:repository")(usersOnly { get("/dashboard/pulls/for/:owner/:repository")(usersOnly {
searchPullRequests("all", Some(params("owner") + "/" + params("repository"))) searchPullRequests("all", Some(params("owner") + "/" + params("repository")))
}) })
private def searchIssues(filter: String) = { private def searchIssues(filter: String) = {
import IssuesService._ import IssuesService._
// condition // condition
val condition = session.putAndGet(Keys.Session.DashboardIssues, val condition = session.putAndGet(Keys.Session.DashboardIssues,
if(request.hasQueryString) IssueSearchCondition(request) if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition()) else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition())
) )
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
val repositories = getUserRepositories(userName, context.baseUrl).map(repo => repo.owner -> repo.name) val repositories = getUserRepositories(userName, context.baseUrl).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName) val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
// //
dashboard.html.issues( dashboard.html.issues(
issues.html.listparts( issues.html.listparts(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, repositories: _*), searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, repositories: _*),
page, page,
countIssue(condition.copy(state = "open"), filterUser, false, repositories: _*), countIssue(condition.copy(state = "open"), filterUser, false, repositories: _*),
countIssue(condition.copy(state = "closed"), filterUser, false, repositories: _*), countIssue(condition.copy(state = "closed"), filterUser, false, repositories: _*),
condition), condition),
countIssue(condition, Map.empty, false, repositories: _*), countIssue(condition, Map.empty, false, repositories: _*),
countIssue(condition, Map("assigned" -> userName), false, repositories: _*), countIssue(condition, Map("assigned" -> userName), false, repositories: _*),
countIssue(condition, Map("created_by" -> userName), false, repositories: _*), countIssue(condition, Map("created_by" -> userName), false, repositories: _*),
countIssueGroupByRepository(condition, filterUser, false, repositories: _*), countIssueGroupByRepository(condition, filterUser, false, repositories: _*),
condition, condition,
filter) filter)
} }
private def searchPullRequests(filter: String, repository: Option[String]) = { private def searchPullRequests(filter: String, repository: Option[String]) = {
import IssuesService._ import IssuesService._
import PullRequestService._ import PullRequestService._
// condition // condition
val condition = session.putAndGet(Keys.Session.DashboardPulls, { val condition = session.putAndGet(Keys.Session.DashboardPulls, {
if(request.hasQueryString) IssueSearchCondition(request) if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](Keys.Session.DashboardPulls).getOrElse(IssueSearchCondition()) else session.getAs[IssueSearchCondition](Keys.Session.DashboardPulls).getOrElse(IssueSearchCondition())
}.copy(repo = repository)) }.copy(repo = repository))
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
val repositories = getUserRepositories(userName, context.baseUrl).map(repo => repo.owner -> repo.name) val repositories = getUserRepositories(userName, context.baseUrl).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName) val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
val counts = countIssueGroupByRepository( val counts = countIssueGroupByRepository(
IssueSearchCondition().copy(state = condition.state), Map.empty, true, repositories: _*) IssueSearchCondition().copy(state = condition.state), Map.empty, true, repositories: _*)
dashboard.html.pulls( dashboard.html.pulls(
pulls.html.listparts( pulls.html.listparts(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, repositories: _*), searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, repositories: _*),
page, page,
countIssue(condition.copy(state = "open"), filterUser, true, repositories: _*), countIssue(condition.copy(state = "open"), filterUser, true, repositories: _*),
countIssue(condition.copy(state = "closed"), filterUser, true, repositories: _*), countIssue(condition.copy(state = "closed"), filterUser, true, repositories: _*),
condition, condition,
None, None,
false), false),
getPullRequestCountGroupByUser(condition.state == "closed", userName, None), getPullRequestCountGroupByUser(condition.state == "closed", None, None),
getRepositoryNamesOfUser(userName).map { RepoName => getRepositoryNamesOfUser(userName).map { RepoName =>
(userName, RepoName, counts.collectFirst { case (_, RepoName, count) => count }.getOrElse(0)) (userName, RepoName, counts.collectFirst { case (_, RepoName, count) => count }.getOrElse(0))
}.sortBy(_._3).reverse, }.sortBy(_._3).reverse,
condition, condition,
filter) filter)
} }
} }

View File

@@ -22,8 +22,8 @@ trait IndexControllerBase extends ControllerBase {
val loginAccount = context.loginAccount val loginAccount = context.loginAccount
html.index(getRecentActivities(), html.index(getRecentActivities(),
getVisibleRepositories(loginAccount, context.baseUrl), getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true),
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl) }.getOrElse(Nil) loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil)
) )
} }

View File

@@ -1,403 +1,403 @@
package app package app
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import service._ import service._
import IssuesService._ import IssuesService._
import util._ import util._
import util.Implicits._ import util.Implicits._
import util.ControlUtil._ import util.ControlUtil._
import org.scalatra.Ok import org.scalatra.Ok
import model.Issue import model.Issue
class IssuesController extends IssuesControllerBase class IssuesController extends IssuesControllerBase
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
trait IssuesControllerBase extends ControllerBase { trait IssuesControllerBase extends ControllerBase {
self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator => with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
case class IssueCreateForm(title: String, content: Option[String], case class IssueCreateForm(title: String, content: Option[String],
assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String]) assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
case class IssueEditForm(title: String, content: Option[String]) case class IssueEditForm(title: String, content: Option[String])
case class CommentForm(issueId: Int, content: String) case class CommentForm(issueId: Int, content: String)
case class IssueStateForm(issueId: Int, content: Option[String]) case class IssueStateForm(issueId: Int, content: Option[String])
val issueCreateForm = mapping( val issueCreateForm = mapping(
"title" -> trim(label("Title", text(required))), "title" -> trim(label("Title", text(required))),
"content" -> trim(optional(text())), "content" -> trim(optional(text())),
"assignedUserName" -> trim(optional(text())), "assignedUserName" -> trim(optional(text())),
"milestoneId" -> trim(optional(number())), "milestoneId" -> trim(optional(number())),
"labelNames" -> trim(optional(text())) "labelNames" -> trim(optional(text()))
)(IssueCreateForm.apply) )(IssueCreateForm.apply)
val issueEditForm = mapping( val issueEditForm = mapping(
"title" -> trim(label("Title", text(required))), "title" -> trim(label("Title", text(required))),
"content" -> trim(optional(text())) "content" -> trim(optional(text()))
)(IssueEditForm.apply) )(IssueEditForm.apply)
val commentForm = mapping( val commentForm = mapping(
"issueId" -> label("Issue Id", number()), "issueId" -> label("Issue Id", number()),
"content" -> trim(label("Comment", text(required))) "content" -> trim(label("Comment", text(required)))
)(CommentForm.apply) )(CommentForm.apply)
val issueStateForm = mapping( val issueStateForm = mapping(
"issueId" -> label("Issue Id", number()), "issueId" -> label("Issue Id", number()),
"content" -> trim(optional(text())) "content" -> trim(optional(text()))
)(IssueStateForm.apply) )(IssueStateForm.apply)
get("/:owner/:repository/issues")(referrersOnly { get("/:owner/:repository/issues")(referrersOnly {
searchIssues("all", _) searchIssues("all", _)
}) })
get("/:owner/:repository/issues/assigned/:userName")(referrersOnly { get("/:owner/:repository/issues/assigned/:userName")(referrersOnly {
searchIssues("assigned", _) searchIssues("assigned", _)
}) })
get("/:owner/:repository/issues/created_by/:userName")(referrersOnly { get("/:owner/:repository/issues/created_by/:userName")(referrersOnly {
searchIssues("created_by", _) searchIssues("created_by", _)
}) })
get("/:owner/:repository/issues/:id")(referrersOnly { repository => get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) => defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) =>
getIssue(owner, name, issueId) map { getIssue(owner, name, issueId) map {
issues.html.issue( issues.html.issue(
_, _,
getComments(owner, name, issueId.toInt), getComments(owner, name, issueId.toInt),
getIssueLabels(owner, name, issueId.toInt), getIssueLabels(owner, name, issueId.toInt),
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestonesWithIssueCount(owner, name), getMilestonesWithIssueCount(owner, name),
getLabels(owner, name), 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 =>
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
issues.html.create( issues.html.create(
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestones(owner, name), getMilestones(owner, name),
getLabels(owner, name), getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount), hasWritePermission(owner, name, context.loginAccount),
repository) repository)
} }
}) })
post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, 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 // record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title) recordCreateIssueActivity(owner, name, userName, issueId, form.title)
// extract references and create refer comment // extract references and create refer comment
getIssue(owner, name, issueId.toString).foreach { issue => getIssue(owner, name, issueId.toString).foreach { issue =>
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
} }
// notifications // notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){ Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
} }
redirect(s"/${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) =>
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
getIssue(owner, name, params("id")).map { issue => getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){ if(isEditable(owner, name, issue.openedUserName)){
// update issue // update issue
updateIssue(owner, name, issue.issueId, form.title, form.content) updateIssue(owner, name, issue.issueId, form.title, form.content)
// extract references and create refer comment // extract references and create refer comment
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized } else Unauthorized
} getOrElse NotFound } getOrElse NotFound
} }
}) })
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) => handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) =>
redirect(s"/${repository.owner}/${repository.name}/${ redirect(s"/${repository.owner}/${repository.name}/${
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
} getOrElse NotFound } getOrElse NotFound
}) })
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
handleComment(form.issueId, form.content, repository)() map { case (issue, id) => handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
redirect(s"/${repository.owner}/${repository.name}/${ redirect(s"/${repository.owner}/${repository.name}/${
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
} getOrElse NotFound } getOrElse NotFound
}) })
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
getComment(owner, name, params("id")).map { comment => getComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){ if(isEditable(owner, name, comment.commentedUserName)){
updateComment(comment.commentId, form.content) updateComment(comment.commentId, form.content)
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}") redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
} else Unauthorized } else Unauthorized
} getOrElse NotFound } getOrElse NotFound
} }
}) })
ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository => ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository =>
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
getComment(owner, name, params("id")).map { comment => getComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){ if(isEditable(owner, name, comment.commentedUserName)){
Ok(deleteComment(comment.commentId)) Ok(deleteComment(comment.commentId))
} else Unauthorized } else Unauthorized
} getOrElse NotFound } getOrElse NotFound
} }
}) })
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository => ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
getIssue(repository.owner, repository.name, params("id")) map { x => getIssue(repository.owner, repository.name, params("id")) map { x =>
if(isEditable(x.userName, x.repositoryName, x.openedUserName)){ if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
params.get("dataType") collect { params.get("dataType") collect {
case t if t == "html" => issues.html.editissue( case t if t == "html" => issues.html.editissue(
x.title, x.content, x.issueId, x.userName, x.repositoryName) x.title, x.content, x.issueId, x.userName, x.repositoryName)
} getOrElse { } getOrElse {
contentType = formats("json") contentType = formats("json")
org.json4s.jackson.Serialization.write( org.json4s.jackson.Serialization.write(
Map("title" -> x.title, Map("title" -> x.title,
"content" -> view.Markdown.toHtml(x.content getOrElse "No description given.", "content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
repository, false, true) repository, false, true)
)) ))
} }
} else Unauthorized } else Unauthorized
} getOrElse NotFound } getOrElse NotFound
}) })
ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository => ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository =>
getComment(repository.owner, repository.name, params("id")) map { x => getComment(repository.owner, repository.name, params("id")) map { x =>
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){ if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
params.get("dataType") collect { params.get("dataType") collect {
case t if t == "html" => issues.html.editcomment( case t if t == "html" => issues.html.editcomment(
x.content, x.commentId, x.userName, x.repositoryName) x.content, x.commentId, x.userName, x.repositoryName)
} getOrElse { } getOrElse {
contentType = formats("json") contentType = formats("json")
org.json4s.jackson.Serialization.write( org.json4s.jackson.Serialization.write(
Map("content" -> view.Markdown.toHtml(x.content, Map("content" -> view.Markdown.toHtml(x.content,
repository, false, true) repository, false, true)
)) ))
} }
} else Unauthorized } else Unauthorized
} getOrElse NotFound } getOrElse NotFound
}) })
ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
defining(params("id").toInt){ issueId => 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 =>
defining(params("id").toInt){ issueId => 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 =>
updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName")) updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName"))
Ok("updated") Ok("updated")
}) })
ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository =>
updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId")) updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
milestoneId("milestoneId").map { milestoneId => milestoneId("milestoneId").map { milestoneId =>
getMilestonesWithIssueCount(repository.owner, repository.name) getMilestonesWithIssueCount(repository.owner, repository.name)
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) => .find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
issues.milestones.html.progress(openCount + closeCount, closeCount, false) issues.milestones.html.progress(openCount + closeCount, closeCount, false)
} getOrElse NotFound } getOrElse NotFound
} getOrElse Ok() } getOrElse Ok()
}) })
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
defining(params.get("value")){ action => 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 =>
params("value").toIntOpt.map{ labelId => params("value").toIntOpt.map{ 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)
} }
} }
} getOrElse NotFound } getOrElse NotFound
}) })
post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository => post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
defining(assignedUserName("value")){ 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 =>
defining(milestoneId("value")){ value => defining(milestoneId("value")){ value =>
executeBatch(repository) { executeBatch(repository) {
updateMilestoneId(repository.owner, repository.name, _, value) updateMilestoneId(repository.owner, repository.name, _, value)
} }
} }
}) })
get("/:owner/:repository/_attached/:file")(referrersOnly { repository => get("/:owner/:repository/_attached/:file")(referrersOnly { repository =>
(Directory.getAttachedDir(repository.owner, repository.name) match { (Directory.getAttachedDir(repository.owner, repository.name) match {
case dir if(dir.exists && dir.isDirectory) => case dir if(dir.exists && dir.isDirectory) =>
dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file => dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file =>
contentType = FileUtil.getMimeType(file.getName) contentType = FileUtil.getMimeType(file.getName)
file file
} }
case _ => None case _ => None
}) getOrElse NotFound }) getOrElse NotFound
}) })
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "") val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt) val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean = private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = { private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
params("checked").split(',') map(_.toInt) foreach execute params("checked").split(',') map(_.toInt) foreach execute
redirect(s"/${repository.owner}/${repository.name}/issues") redirect(s"/${repository.owner}/${repository.name}/issues")
} }
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = { private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
StringUtil.extractIssueId(message).foreach { issueId => StringUtil.extractIssueId(message).foreach { issueId =>
if(getIssue(owner, repository, issueId).isDefined){ if(getIssue(owner, repository, issueId).isDefined){
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt,
fromIssue.issueId + ":" + fromIssue.title, "refer") fromIssue.issueId + ":" + fromIssue.title, "refer")
} }
} }
} }
/** /**
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]] * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
*/ */
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))) = {
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
getIssue(owner, name, issueId.toString) map { issue => getIssue(owner, name, issueId.toString) map { issue =>
val (action, recordActivity) = val (action, recordActivity) =
getAction(issue) getAction(issue)
.collect { .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) )
// extract references and create refer comment // extract references and create refer comment
content.map { content => content.map { content =>
createReferComment(owner, name, issue, content) createReferComment(owner, name, issue, content)
} }
// 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"${context.baseUrl}/${owner}/${name}/${ Notifier.msgComment(s"${context.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"${context.baseUrl}/${owner}/${name}/issues/${issueId}") Notifier.msgStatus(s"${context.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) = {
defining(repository.owner, repository.name){ case (owner, repoName) => defining(repository.owner, repository.name){ case (owner, repoName) =>
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 = Keys.Session.Issues(owner, repoName)
// retrieve search condition // retrieve search condition
val condition = session.putAndGet(sessionKey, val condition = session.putAndGet(sessionKey,
if(request.hasQueryString) IssueSearchCondition(request) if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition()) else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
) )
issues.html.list( issues.html.list(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page, page,
(getCollaborators(owner, repoName) :+ owner).sorted, (getCollaborators(owner, repoName) :+ owner).sorted,
getMilestones(owner, repoName), getMilestones(owner, repoName),
getLabels(owner, repoName), getLabels(owner, repoName),
countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName), countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName),
countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName), countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName),
countIssue(condition, Map.empty, false, owner -> repoName), countIssue(condition, Map.empty, false, owner -> repoName),
context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)), context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)),
context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)), context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)),
countIssueGroupByLabels(owner, repoName, condition, filterUser), countIssueGroupByLabels(owner, repoName, condition, filterUser),
condition, condition,
filter, filter,
repository, repository,
hasWritePermission(owner, repoName, context.loginAccount)) hasWritePermission(owner, repoName, context.loginAccount))
} }
} }
} }

View File

@@ -13,7 +13,6 @@ import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
import service.IssuesService._ import service.IssuesService._
import service.PullRequestService._ import service.PullRequestService._
import util.JGitUtil.DiffInfo import util.JGitUtil.DiffInfo
import service.RepositoryService.RepositoryTreeNode
import util.JGitUtil.CommitInfo import util.JGitUtil.CommitInfo
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.eclipse.jgit.merge.MergeStrategy import org.eclipse.jgit.merge.MergeStrategy
@@ -124,7 +123,7 @@ trait PullRequestsControllerBase extends ControllerBase {
params("id").toIntOpt.flatMap { issueId => params("id").toIntOpt.flatMap { issueId =>
val owner = repository.owner val owner = repository.owner
val name = repository.name val name = repository.name
LockUtil.lock(s"${owner}/${name}/merge"){ LockUtil.lock(s"${owner}/${name}"){
getPullRequest(owner, name, issueId).map { case (issue, pullreq) => getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
using(Git.open(getRepositoryDir(owner, name))) { git => using(Git.open(getRepositoryDir(owner, name))) { git =>
// mark issue as merged and close. // mark issue as merged and close.
@@ -367,7 +366,7 @@ trait PullRequestsControllerBase extends ControllerBase {
*/ */
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 = {
LockUtil.lock(s"${userName}/${repositoryName}/merge-check"){ LockUtil.lock(s"${userName}/${repositoryName}"){
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git => using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
val remoteRefName = s"refs/heads/${branch}" val remoteRefName = s"refs/heads/${branch}"
val tmpRefName = s"refs/merge-check/${userName}/${branch}" val tmpRefName = s"refs/merge-check/${userName}/${branch}"
@@ -403,7 +402,7 @@ trait PullRequestsControllerBase extends ControllerBase {
private def checkConflictInPullRequest(userName: String, repositoryName: String, branch: String, private def checkConflictInPullRequest(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String,
issueId: Int): Boolean = { issueId: Int): Boolean = {
LockUtil.lock(s"${userName}/${repositoryName}/merge") { LockUtil.lock(s"${userName}/${repositoryName}") {
using(Git.open(getRepositoryDir(userName, repositoryName))) { git => using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
// merge // merge
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true) val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
@@ -466,7 +465,7 @@ trait PullRequestsControllerBase extends ControllerBase {
pulls.html.list( pulls.html.list(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
getPullRequestCountGroupByUser(condition.state == "closed", owner, Some(repoName)), getPullRequestCountGroupByUser(condition.state == "closed", Some(owner), Some(repoName)),
userName, userName,
page, page,
countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName), countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName),

View File

@@ -4,12 +4,14 @@ import service._
import util.Directory._ import util.Directory._
import util.ControlUtil._ import util.ControlUtil._
import util.Implicits._ import util.Implicits._
import util.{UsersAuthenticator, OwnerAuthenticator} import util.{LockUtil, UsersAuthenticator, OwnerAuthenticator}
import util.JGitUtil.CommitInfo import util.JGitUtil.CommitInfo
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.i18n.Messages import org.scalatra.i18n.Messages
import service.WebHookService.WebHookPayload import service.WebHookService.WebHookPayload
import util.JGitUtil.CommitInfo
import util.ControlUtil._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
class RepositorySettingsController extends RepositorySettingsControllerBase class RepositorySettingsController extends RepositorySettingsControllerBase
@@ -186,15 +188,17 @@ trait RepositorySettingsControllerBase extends ControllerBase {
post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) => post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) =>
// Change repository owner // Change repository owner
if(repository.owner != form.newOwner){ if(repository.owner != form.newOwner){
// Update database LockUtil.lock(s"${repository.owner}/${repository.name}"){
renameRepository(repository.owner, repository.name, form.newOwner, repository.name) // Update database
// Move git repository renameRepository(repository.owner, repository.name, form.newOwner, repository.name)
defining(getRepositoryDir(repository.owner, repository.name)){ dir => // Move git repository
FileUtils.moveDirectory(dir, getRepositoryDir(form.newOwner, repository.name)) defining(getRepositoryDir(repository.owner, repository.name)){ dir =>
} FileUtils.moveDirectory(dir, getRepositoryDir(form.newOwner, repository.name))
// Move wiki repository }
defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir => // Move wiki repository
FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name)) defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name))
}
} }
} }
redirect(s"/${form.newOwner}/${repository.name}") redirect(s"/${form.newOwner}/${repository.name}")
@@ -204,12 +208,13 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Delete the repository. * Delete the repository.
*/ */
post("/:owner/:repository/settings/delete")(ownerOnly { repository => post("/:owner/:repository/settings/delete")(ownerOnly { repository =>
deleteRepository(repository.owner, repository.name) LockUtil.lock(s"${repository.owner}/${repository.name}"){
deleteRepository(repository.owner, repository.name)
FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name))
}
redirect(s"/${repository.owner}") redirect(s"/${repository.owner}")
}) })

View File

@@ -3,8 +3,13 @@ package app
import service.{AccountService, SystemSettingsService} import service.{AccountService, SystemSettingsService}
import SystemSettingsService._ import SystemSettingsService._
import util.AdminAuthenticator import util.AdminAuthenticator
import util.Directory._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import ssh.SshServer import ssh.SshServer
import org.apache.commons.io.FileUtils
import java.io.FileInputStream
import plugin.{Plugin, PluginSystem}
class SystemSettingsController extends SystemSettingsControllerBase class SystemSettingsController extends SystemSettingsControllerBase
with AccountService with AdminAuthenticator with AccountService with AdminAuthenticator
@@ -47,6 +52,11 @@ trait SystemSettingsControllerBase extends ControllerBase {
} else Nil } else Nil
} }
private val pluginForm = mapping(
"pluginId" -> list(trim(label("", text())))
)(PluginForm.apply)
case class PluginForm(pluginIds: List[String])
get("/admin/system")(adminOnly { get("/admin/system")(adminOnly {
admin.html.system(flash.get("info")) admin.html.system(flash.get("info"))
@@ -71,4 +81,104 @@ trait SystemSettingsControllerBase extends ControllerBase {
redirect("/admin/system") redirect("/admin/system")
}) })
// TODO Enable commented code to enable plug-in system
// get("/admin/plugins")(adminOnly {
// val installedPlugins = plugin.PluginSystem.plugins
// val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable")
// admin.plugins.html.installed(installedPlugins, updatablePlugins)
// })
//
// post("/admin/plugins/_update", pluginForm)(adminOnly { form =>
// deletePlugins(form.pluginIds)
// installPlugins(form.pluginIds)
// redirect("/admin/plugins")
// })
//
// post("/admin/plugins/_delete", pluginForm)(adminOnly { form =>
// deletePlugins(form.pluginIds)
// redirect("/admin/plugins")
// })
//
// get("/admin/plugins/available")(adminOnly {
// val installedPlugins = plugin.PluginSystem.plugins
// val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available")
// admin.plugins.html.available(availablePlugins)
// })
//
// post("/admin/plugins/_install", pluginForm)(adminOnly { form =>
// installPlugins(form.pluginIds)
// redirect("/admin/plugins")
// })
// get("/admin/plugins/console")(adminOnly {
// admin.plugins.html.console()
// })
//
// post("/admin/plugins/console")(adminOnly {
// val script = request.getParameter("script")
// val result = plugin.JavaScriptPlugin.evaluateJavaScript(script)
// Ok(result)
// })
// TODO Move these methods to PluginSystem or Service?
private def deletePlugins(pluginIds: List[String]): Unit = {
pluginIds.foreach { pluginId =>
plugin.PluginSystem.uninstall(pluginId)
val dir = new java.io.File(PluginHome, pluginId)
if(dir.exists && dir.isDirectory){
FileUtils.deleteQuietly(dir)
PluginSystem.uninstall(pluginId)
}
}
}
private def installPlugins(pluginIds: List[String]): Unit = {
val dir = getPluginCacheDir()
val installedPlugins = plugin.PluginSystem.plugins
getAvailablePlugins(installedPlugins).filter(x => pluginIds.contains(x.id)).foreach { plugin =>
val pluginDir = new java.io.File(PluginHome, plugin.id)
if(!pluginDir.exists){
FileUtils.copyDirectory(new java.io.File(dir, plugin.repository + "/" + plugin.id), pluginDir)
}
PluginSystem.installPlugin(plugin.id)
}
}
private def getAvailablePlugins(installedPlugins: List[Plugin]): List[SystemSettingsControllerBase.AvailablePlugin] = {
val repositoryRoot = getPluginCacheDir()
if(repositoryRoot.exists && repositoryRoot.isDirectory){
PluginSystem.repositories.flatMap { repo =>
val repoDir = new java.io.File(repositoryRoot, repo.id)
if(repoDir.exists && repoDir.isDirectory){
repoDir.listFiles.filter(d => d.isDirectory && !d.getName.startsWith(".")).map { plugin =>
val propertyFile = new java.io.File(plugin, "plugin.properties")
val properties = new java.util.Properties()
if(propertyFile.exists && propertyFile.isFile){
using(new FileInputStream(propertyFile)){ in =>
properties.load(in)
}
}
SystemSettingsControllerBase.AvailablePlugin(
repository = repo.id,
id = properties.getProperty("id"),
version = properties.getProperty("version"),
author = properties.getProperty("author"),
url = properties.getProperty("url"),
description = properties.getProperty("description"),
status = installedPlugins.find(_.id == properties.getProperty("id")) match {
case Some(x) if(PluginSystem.isUpdatable(x.version, properties.getProperty("version")))=> "updatable"
case Some(x) => "installed"
case None => "available"
})
}
} else Nil
}
} else Nil
}
}
object SystemSettingsControllerBase {
case class AvailablePlugin(repository: String, id: String, version: String,
author: String, url: String, description: String, status: String)
} }

View File

@@ -23,6 +23,7 @@ trait AccountComponent { self: Profile =>
} }
case class Account( case class Account(
userName: String, userName: String,
fullName: String, fullName: String,
mailAddress: String, mailAddress: String,

View File

@@ -0,0 +1,88 @@
package plugin
import org.mozilla.javascript.{Context => JsContext}
import org.mozilla.javascript.{Function => JsFunction}
import scala.collection.mutable.ListBuffer
import plugin.PluginSystem.{Action, GlobalMenu, RepositoryMenu}
class JavaScriptPlugin(val id: String, val version: String,
val author: String, val url: String, val description: String) extends Plugin {
private val repositoryMenuList = ListBuffer[RepositoryMenu]()
private val globalMenuList = ListBuffer[GlobalMenu]()
private val repositoryActionList = ListBuffer[Action]()
private val globalActionList = ListBuffer[Action]()
def repositoryMenus : List[RepositoryMenu] = repositoryMenuList.toList
def globalMenus : List[GlobalMenu] = globalMenuList.toList
def repositoryActions : List[Action] = repositoryActionList.toList
def globalActions : List[Action] = globalActionList.toList
def addRepositoryMenu(label: String, name: String, url: String, icon: String, condition: JsFunction): Unit = {
repositoryMenuList += RepositoryMenu(label, name, url, icon, (context) => {
val context = JsContext.enter()
try {
condition.call(context, condition, condition, Array(context)).asInstanceOf[Boolean]
} finally {
JsContext.exit()
}
})
}
def addGlobalMenu(label: String, url: String, icon: String, condition: JsFunction): Unit = {
globalMenuList += GlobalMenu(label, url, icon, (context) => {
val context = JsContext.enter()
try {
condition.call(context, condition, condition, Array(context)).asInstanceOf[Boolean]
} finally {
JsContext.exit()
}
})
}
def addGlobalAction(path: String, function: JsFunction): Unit = {
globalActionList += Action(path, (request, response) => {
val context = JsContext.enter()
try {
function.call(context, function, function, Array(request, response))
} finally {
JsContext.exit()
}
})
}
def addRepositoryAction(path: String, function: JsFunction): Unit = {
repositoryActionList += Action(path, (request, response) => {
val context = JsContext.enter()
try {
function.call(context, function, function, Array(request, response))
} finally {
JsContext.exit()
}
})
}
}
object JavaScriptPlugin {
def define(id: String, version: String, author: String, url: String, description: String)
= new JavaScriptPlugin(id, version, author, url, description)
def evaluateJavaScript(script: String, vars: Map[String, Any] = Map.empty): Any = {
val context = JsContext.enter()
try {
val scope = context.initStandardObjects()
scope.put("PluginSystem", scope, PluginSystem)
scope.put("JavaScriptPlugin", scope, this)
vars.foreach { case (key, value) =>
scope.put(key, scope, value)
}
val result = context.evaluateString(scope, script, "<cmd>", 1, null)
result
} finally {
JsContext.exit
}
}
}

View File

@@ -0,0 +1,16 @@
package plugin
import plugin.PluginSystem.{Action, GlobalMenu, RepositoryMenu}
trait Plugin {
val id: String
val version: String
val author: String
val url: String
val description: String
def repositoryMenus : List[RepositoryMenu]
def globalMenus : List[GlobalMenu]
def repositoryActions : List[Action]
def globalActions : List[Action]
}

View File

@@ -0,0 +1,123 @@
package plugin
import app.Context
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import org.slf4j.LoggerFactory
import java.util.concurrent.atomic.AtomicBoolean
import util.Directory._
import util.ControlUtil._
import org.apache.commons.io.FileUtils
import util.JGitUtil
import org.eclipse.jgit.api.Git
/**
* Provides extension points to plug-ins.
*/
object PluginSystem {
private val logger = LoggerFactory.getLogger(PluginSystem.getClass)
private val initialized = new AtomicBoolean(false)
private val pluginsMap = scala.collection.mutable.Map[String, Plugin]()
private val repositoriesList = scala.collection.mutable.ListBuffer[PluginRepository]()
def install(plugin: Plugin): Unit = {
pluginsMap.put(plugin.id, plugin)
}
def plugins: List[Plugin] = pluginsMap.values.toList
def uninstall(id: String): Unit = {
pluginsMap.remove(id)
}
def repositories: List[PluginRepository] = repositoriesList.toList
/**
* Initializes the plugin system. Load scripts from GITBUCKET_HOME/plugins.
*/
def init(): Unit = {
if(initialized.compareAndSet(false, true)){
// Load installed plugins
val pluginDir = new java.io.File(PluginHome)
if(pluginDir.exists && pluginDir.isDirectory){
pluginDir.listFiles.filter(f => f.isDirectory && !f.getName.startsWith(".")).foreach { dir =>
installPlugin(dir.getName)
}
}
// Add default plugin repositories
repositoriesList += PluginRepository("central", "https://github.com/takezoe/gitbucket_plugins.git")
}
}
// TODO Method name seems to not so good.
def installPlugin(id: String): Unit = {
val pluginDir = new java.io.File(PluginHome)
val javaScriptFile = new java.io.File(pluginDir, id + "/plugin.js")
if(javaScriptFile.exists && javaScriptFile.isFile){
val properties = new java.util.Properties()
using(new java.io.FileInputStream(new java.io.File(pluginDir, id + "/plugin.properties"))){ in =>
properties.load(in)
}
val script = FileUtils.readFileToString(javaScriptFile, "UTF-8")
try {
JavaScriptPlugin.evaluateJavaScript(script, Map(
"id" -> properties.getProperty("id"),
"version" -> properties.getProperty("version"),
"author" -> properties.getProperty("author"),
"url" -> properties.getProperty("url"),
"description" -> properties.getProperty("description")
))
} catch {
case e: Exception => logger.warn(s"Error in plugin loading for ${javaScriptFile.getAbsolutePath}", e)
}
}
}
def repositoryMenus : List[RepositoryMenu] = pluginsMap.values.flatMap(_.repositoryMenus).toList
def globalMenus : List[GlobalMenu] = pluginsMap.values.flatMap(_.globalMenus).toList
def repositoryActions : List[Action] = pluginsMap.values.flatMap(_.repositoryActions).toList
def globalActions : List[Action] = pluginsMap.values.flatMap(_.globalActions).toList
// Case classes to hold plug-ins information internally in GitBucket
case class PluginRepository(id: String, url: String)
case class GlobalMenu(label: String, url: String, icon: String, condition: Context => Boolean)
case class RepositoryMenu(label: String, name: String, url: String, icon: String, condition: Context => Boolean)
case class Action(path: String, function: (HttpServletRequest, HttpServletResponse) => Any)
/**
* Checks whether the plugin is updatable.
*/
def isUpdatable(oldVersion: String, newVersion: String): Boolean = {
if(oldVersion == newVersion){
false
} else {
val dim1 = oldVersion.split("\\.").map(_.toInt)
val dim2 = newVersion.split("\\.").map(_.toInt)
dim1.zip(dim2).foreach { case (a, b) =>
if(a < b){
return true
} else if(a > b){
return false
}
}
return false
}
}
// TODO This is a test
// addGlobalMenu("Google", "http://www.google.co.jp/", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAEvwAABL8BkeKJvAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAIgSURBVEiJtdZNiI1hFAfw36ORhSFFPgYLszOKJAsWRLGzks1gYyFZKFs7C7K2Y2XDRiwmq9kIJWQjJR9Tk48xRtTIRwjH4p473nm99yLNqdNTz/mf//+555x7ektEmEmbNaPs6OkUKKX0YBmWp6/IE8bwIs8xjEfEt0aiiJBl6sEuXMRLfEf8pX/PnIvJ0TPFWxE4+w+Ef/Kzbd5qDx5l8H8tkku7LG17gH7sxWatevdhEUoXsjda5RnDTZzH6jagtMe0lHIa23AJw3iOiSRZlmJ9mfcyfTzFl2AldmI3rkbEkbrAYKrX7S1eVRyWVnxhQ87eiLjQ+o2/mtyve+PuYy3W4+EfsP2/TVGKTHRI+Iz9Fdx8XOmAnZjGWRMYqoF/4ESW4hpOYk1iZ2WsLjDUTeBYBfgeuyux2XiNT5hXud+DD5W8Y90EtifoSfultfjx7MVtrKzcr8No5m7vJtCLx1hQJ8/4IZzClpyoy5ibsYUYQW81Z9o2jYgPeKr15+poEXE9+1XF9WIkOaasaV2P4k4pZUdDbEm+VEQcjIgtEfGxlLIVd/Gs6TX1MhzQquU3HK1t23f4IsuS94fxNXMO/MbXIDBg+tidw5yMbcCmylSdqWEH/kagYLKWeAt9Fcxi3KhhJuXq6SqQBMO15NDalvswmLWux4cbuToIbMS9BpJOfg8bm7imtmmTlVJWaa3hpnU9nufziBjtyDHTny0/AaA7Qnb4AM4aAAAAAElFTkSuQmCC")
// { context => context.loginAccount.isDefined }
//
// addRepositoryMenu("Board", "board", "/board", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAEvwAABL8BkeKJvAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAIgSURBVEiJtdZNiI1hFAfw36ORhSFFPgYLszOKJAsWRLGzks1gYyFZKFs7C7K2Y2XDRiwmq9kIJWQjJR9Tk48xRtTIRwjH4p473nm99yLNqdNTz/mf//+555x7ektEmEmbNaPs6OkUKKX0YBmWp6/IE8bwIs8xjEfEt0aiiJBl6sEuXMRLfEf8pX/PnIvJ0TPFWxE4+w+Ef/Kzbd5qDx5l8H8tkku7LG17gH7sxWatevdhEUoXsjda5RnDTZzH6jagtMe0lHIa23AJw3iOiSRZlmJ9mfcyfTzFl2AldmI3rkbEkbrAYKrX7S1eVRyWVnxhQ87eiLjQ+o2/mtyve+PuYy3W4+EfsP2/TVGKTHRI+Iz9Fdx8XOmAnZjGWRMYqoF/4ESW4hpOYk1iZ2WsLjDUTeBYBfgeuyux2XiNT5hXud+DD5W8Y90EtifoSfultfjx7MVtrKzcr8No5m7vJtCLx1hQJ8/4IZzClpyoy5ibsYUYQW81Z9o2jYgPeKr15+poEXE9+1XF9WIkOaasaV2P4k4pZUdDbEm+VEQcjIgtEfGxlLIVd/Gs6TX1MhzQquU3HK1t23f4IsuS94fxNXMO/MbXIDBg+tidw5yMbcCmylSdqWEH/kagYLKWeAt9Fcxi3KhhJuXq6SqQBMO15NDalvswmLWux4cbuToIbMS9BpJOfg8bm7imtmmTlVJWaa3hpnU9nufziBjtyDHTny0/AaA7Qnb4AM4aAAAAAElFTkSuQmCC")
// { context => true}
//
// addGlobalAction("/hello"){ (request, response) =>
// "Hello World!"
// }
}

View File

@@ -0,0 +1,67 @@
package plugin
import util.Directory._
import org.eclipse.jgit.api.Git
import org.slf4j.LoggerFactory
import org.quartz.{Scheduler, JobExecutionContext, Job}
import org.quartz.JobBuilder._
import org.quartz.TriggerBuilder._
import org.quartz.SimpleScheduleBuilder._
class PluginUpdateJob extends Job {
private val logger = LoggerFactory.getLogger(classOf[PluginUpdateJob])
private var failedCount = 0
/**
* Clone or pull all plugin repositories
*
* TODO Support plugin repository access through the proxy server
*/
override def execute(context: JobExecutionContext): Unit = {
try {
if(failedCount > 3){
logger.error("Skip plugin information updating because failed count is over limit")
} else {
logger.info("Start plugin information updating")
PluginSystem.repositories.foreach { repository =>
logger.info(s"Updating ${repository.id}: ${repository.url}...")
val dir = getPluginCacheDir()
val repo = new java.io.File(dir, repository.id)
if(repo.exists){
// pull if the repository is already cloned
Git.open(repo).pull().call()
} else {
// clone if the repository is not exist
Git.cloneRepository().setURI(repository.url).setDirectory(repo).call()
}
}
logger.info("End plugin information updating")
}
} catch {
case e: Exception => {
failedCount = failedCount + 1
logger.error("Failed to update plugin information", e)
}
}
}
}
object PluginUpdateJob {
def schedule(scheduler: Scheduler): Unit = {
// TODO Enable commented code to enable plug-in system
// val job = newJob(classOf[PluginUpdateJob])
// .withIdentity("pluginUpdateJob")
// .build()
//
// val trigger = newTrigger()
// .withIdentity("pluginUpdateTrigger")
// .startNow()
// .withSchedule(simpleSchedule().withIntervalInHours(24).repeatForever())
// .build()
//
// scheduler.scheduleJob(job, trigger)
}
}

View File

@@ -0,0 +1,38 @@
package plugin
import app.Context
import scala.collection.mutable.ListBuffer
import plugin.PluginSystem.{Action, GlobalMenu, RepositoryMenu}
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
// TODO This is a sample implementation for Scala based plug-ins.
class ScalaPlugin(val id: String, val version: String,
val author: String, val url: String, val description: String) extends Plugin {
private val repositoryMenuList = ListBuffer[RepositoryMenu]()
private val globalMenuList = ListBuffer[GlobalMenu]()
private val repositoryActionList = ListBuffer[Action]()
private val globalActionList = ListBuffer[Action]()
def repositoryMenus : List[RepositoryMenu] = repositoryMenuList.toList
def globalMenus : List[GlobalMenu] = globalMenuList.toList
def repositoryActions : List[Action] = repositoryActionList.toList
def globalActions : List[Action] = globalActionList.toList
def addRepositoryMenu(label: String, name: String, url: String, icon: String)(condition: (Context) => Boolean): Unit = {
repositoryMenuList += RepositoryMenu(label, name, url, icon, condition)
}
def addGlobalMenu(label: String, url: String, icon: String)(condition: (Context) => Boolean): Unit = {
globalMenuList += GlobalMenu(label, url, icon, condition)
}
def addGlobalAction(path: String)(function: (HttpServletRequest, HttpServletResponse) => Any): Unit = {
globalActionList += Action(path, function)
}
def addRepositoryAction(path: String)(function: (HttpServletRequest, HttpServletResponse) => Any): Unit = {
repositoryActionList += Action(path, function)
}
}

View File

@@ -51,6 +51,7 @@ trait IssuesService {
repos: (String, String)*)(implicit s: Session): Int = repos: (String, String)*)(implicit s: Session): Int =
// TODO check SQL // TODO check SQL
Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first
/** /**
* Returns the Map which contains issue count for each labels. * Returns the Map which contains issue count for each labels.
* *

View File

@@ -20,13 +20,13 @@ trait PullRequestService { self: IssuesService =>
.map(pr => pr.commitIdTo -> pr.commitIdFrom) .map(pr => pr.commitIdTo -> pr.commitIdFrom)
.update((commitIdTo, commitIdFrom)) .update((commitIdTo, commitIdFrom))
def getPullRequestCountGroupByUser(closed: Boolean, owner: String, repository: Option[String]) def getPullRequestCountGroupByUser(closed: Boolean, owner: Option[String], repository: Option[String])
(implicit s: Session): List[PullRequestCount] = (implicit s: Session): List[PullRequestCount] =
PullRequests PullRequests
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) } .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
.filter { case (t1, t2) => .filter { case (t1, t2) =>
(t2.closed is closed.bind) && (t2.closed is closed.bind) &&
(t1.userName is owner.bind) && (t1.userName is owner.get.bind, owner.isDefined) &&
(t1.repositoryName is repository.get.bind, repository.isDefined) (t1.repositoryName is repository.get.bind, repository.isDefined)
} }
.groupBy { case (t1, t2) => t2.openedUserName } .groupBy { case (t1, t2) => t2.openedUserName }

View File

@@ -156,13 +156,18 @@ trait RepositoryService { self: AccountService =>
} }
} }
def getUserRepositories(userName: String, baseUrl: String)(implicit s: Session): List[RepositoryInfo] = { def getUserRepositories(userName: String, baseUrl: String, withoutPhysicalInfo: Boolean = false)
(implicit s: Session): List[RepositoryInfo] = {
Repositories.filter { t1 => Repositories.filter { t1 =>
(t1.userName is userName.bind) || (t1.userName is userName.bind) ||
(Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName is userName.bind)} exists) (Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName is userName.bind)} exists)
}.sortBy(_.lastActivityDate desc).list.map{ repository => }.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo( new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), if(withoutPhysicalInfo){
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
} else {
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
},
repository, repository,
getForkedCount( getForkedCount(
repository.originUserName.getOrElse(repository.userName), repository.originUserName.getOrElse(repository.userName),
@@ -179,9 +184,12 @@ trait RepositoryService { self: AccountService =>
* @param loginAccount the logged in account * @param loginAccount the logged in account
* @param baseUrl the base url of this application * @param baseUrl the base url of this application
* @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user) * @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
* @param withoutPhysicalInfo if true then the result does not include physical repository information such as commit count,
* branches and tags
* @return the repository information which is sorted in descending order of lastActivityDate. * @return the repository information which is sorted in descending order of lastActivityDate.
*/ */
def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None) def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None,
withoutPhysicalInfo: Boolean = false)
(implicit s: Session): List[RepositoryInfo] = { (implicit s: Session): List[RepositoryInfo] = {
(loginAccount match { (loginAccount match {
// for Administrators // for Administrators
@@ -197,7 +205,11 @@ trait RepositoryService { self: AccountService =>
repositoryUserName.map { userName => t.userName is userName.bind } getOrElse LiteralColumn(true) repositoryUserName.map { userName => t.userName is userName.bind } getOrElse LiteralColumn(true)
}.sortBy(_.lastActivityDate desc).list.map{ repository => }.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo( new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), if(withoutPhysicalInfo){
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
} else {
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
},
repository, repository,
getForkedCount( getForkedCount(
repository.originUserName.getOrElse(repository.userName), repository.originUserName.getOrElse(repository.userName),

View File

@@ -1,193 +1,209 @@
package servlet package servlet
import java.io.File import java.io.File
import java.sql.{DriverManager, Connection} import java.sql.{DriverManager, Connection}
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import javax.servlet.{ServletContext, ServletContextListener, ServletContextEvent} import javax.servlet.{ServletContext, ServletContextListener, 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 util.ControlUtil._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import util.Directory import util.Directory
import plugin.PluginUpdateJob
object AutoUpdate {
object AutoUpdate {
/**
* Version of GitBucket /**
* * Version of GitBucket
* @param majorVersion the major version *
* @param minorVersion the minor version * @param majorVersion the major version
*/ * @param minorVersion the minor version
case class Version(majorVersion: Int, minorVersion: Int){ */
case class Version(majorVersion: Int, minorVersion: Int){
private val logger = LoggerFactory.getLogger(classOf[servlet.AutoUpdate.Version])
private val logger = LoggerFactory.getLogger(classOf[servlet.AutoUpdate.Version])
/**
* Execute update/MAJOR_MINOR.sql to update schema to this version. /**
* If corresponding SQL file does not exist, this method do nothing. * Execute update/MAJOR_MINOR.sql to update schema to this version.
*/ * If corresponding SQL file does not exist, this method do nothing.
def update(conn: Connection): Unit = { */
val sqlPath = s"update/${majorVersion}_${minorVersion}.sql" def update(conn: Connection): Unit = {
val sqlPath = s"update/${majorVersion}_${minorVersion}.sql"
using(Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)){ in =>
if(in != null){ using(Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)){ in =>
val sql = IOUtils.toString(in, "UTF-8") if(in != null){
using(conn.createStatement()){ stmt => val sql = IOUtils.toString(in, "UTF-8")
logger.debug(sqlPath + "=" + sql) using(conn.createStatement()){ stmt =>
stmt.executeUpdate(sql) logger.debug(sqlPath + "=" + sql)
} stmt.executeUpdate(sql)
} }
} }
} }
}
/**
* MAJOR.MINOR /**
*/ * MAJOR.MINOR
val versionString = s"${majorVersion}.${minorVersion}" */
} val versionString = s"${majorVersion}.${minorVersion}"
}
/**
* 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( */
new Version(2, 0){ val versions = Seq(
override def update(conn: Connection): Unit = { new Version(2, 0){
import eu.medsea.mimeutil.{MimeUtil2, MimeType} override def update(conn: Connection): Unit = {
import eu.medsea.mimeutil.{MimeUtil2, MimeType}
val mimeUtil = new MimeUtil2()
mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector") val mimeUtil = new MimeUtil2()
mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
super.update(conn)
using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs => super.update(conn)
while(rs.next){ using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir => while(rs.next){
if(dir.exists && dir.isDirectory){ defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
dir.listFiles.foreach { file => if(dir.exists && dir.isDirectory){
if(file.getName.indexOf('.') < 0){ dir.listFiles.foreach { file =>
val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString if(file.getName.indexOf('.') < 0){
if(mimeType.startsWith("image/")){ val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString
file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1))) if(mimeType.startsWith("image/")){
} file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1)))
} }
} }
} }
} }
} }
} }
} }
}, }
Version(1, 13), },
Version(1, 12), Version(1, 13),
Version(1, 11), Version(1, 12),
Version(1, 10), Version(1, 11),
Version(1, 9), Version(1, 10),
Version(1, 8), Version(1, 9),
Version(1, 7), Version(1, 8),
Version(1, 6), Version(1, 7),
Version(1, 5), Version(1, 6),
Version(1, 4), Version(1, 5),
new Version(1, 3){ Version(1, 4),
override def update(conn: Connection): Unit = { new Version(1, 3){
super.update(conn) override def update(conn: Connection): Unit = {
// Fix wiki repository configuration super.update(conn)
using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs => // Fix wiki repository configuration
while(rs.next){ using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git => while(rs.next){
defining(git.getRepository.getConfig){ config => using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
if(!config.getBoolean("http", "receivepack", false)){ defining(git.getRepository.getConfig){ config =>
config.setBoolean("http", null, "receivepack", true) if(!config.getBoolean("http", "receivepack", false)){
config.save config.setBoolean("http", null, "receivepack", true)
} config.save
} }
} }
} }
} }
} }
}, }
Version(1, 2), },
Version(1, 1), Version(1, 2),
Version(1, 0), Version(1, 1),
Version(0, 0) Version(1, 0),
) Version(0, 0)
)
/**
* The head version of BitBucket. /**
*/ * The head version of BitBucket.
val headVersion = versions.head */
val headVersion = versions.head
/**
* The version file (GITBUCKET_HOME/version). /**
*/ * The version file (GITBUCKET_HOME/version).
lazy val versionFile = new File(GitBucketHome, "version") */
lazy val versionFile = new File(GitBucketHome, "version")
/**
* Returns the current version from the version file. /**
*/ * Returns the current version from the version file.
def getCurrentVersion(): Version = { */
if(versionFile.exists){ def getCurrentVersion(): Version = {
FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match { if(versionFile.exists){
case Array(majorVersion, minorVersion) => { FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match {
versions.find { v => case Array(majorVersion, minorVersion) => {
v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt versions.find { v =>
}.getOrElse(Version(0, 0)) v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt
} }.getOrElse(Version(0, 0))
case _ => Version(0, 0) }
} case _ => Version(0, 0)
} else Version(0, 0) }
} } else Version(0, 0)
}
}
}
/**
* Update database schema automatically in the context initializing. /**
*/ * Update database schema automatically in the context initializing.
class AutoUpdateListener extends ServletContextListener { */
import AutoUpdate._ class AutoUpdateListener extends ServletContextListener {
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener]) import org.quartz.impl.StdSchedulerFactory
import org.quartz.JobBuilder._
override def contextInitialized(event: ServletContextEvent): Unit = { import org.quartz.TriggerBuilder._
val datadir = event.getServletContext.getInitParameter("gitbucket.home") import org.quartz.SimpleScheduleBuilder._
if(datadir != null){ import AutoUpdate._
System.setProperty("gitbucket.home", datadir)
} private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
org.h2.Driver.load() private val scheduler = StdSchedulerFactory.getDefaultScheduler
event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true")
override def contextInitialized(event: ServletContextEvent): Unit = {
logger.debug("Start schema update") val datadir = event.getServletContext.getInitParameter("gitbucket.home")
defining(getConnection(event.getServletContext)){ conn => if(datadir != null){
try { System.setProperty("gitbucket.home", datadir)
defining(getCurrentVersion()){ currentVersion => }
if(currentVersion == headVersion){ org.h2.Driver.load()
logger.debug("No update") event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true")
} else if(!versions.contains(currentVersion)){
logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.") logger.debug("Start schema update")
} else { defining(getConnection(event.getServletContext)){ conn =>
versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn)) try {
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8") defining(getCurrentVersion()){ currentVersion =>
conn.commit() if(currentVersion == headVersion){
logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}") logger.debug("No update")
} } else if(!versions.contains(currentVersion)){
} logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.")
} catch { } else {
case ex: Throwable => { versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
logger.error("Failed to schema update", ex) FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
ex.printStackTrace() conn.commit()
conn.rollback() logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}")
} }
} }
} } catch {
logger.debug("End schema update") case ex: Throwable => {
} logger.error("Failed to schema update", ex)
ex.printStackTrace()
def contextDestroyed(sce: ServletContextEvent): Unit = { conn.rollback()
// Nothing to do. }
} }
}
private def getConnection(servletContext: ServletContext): Connection = logger.debug("End schema update")
DriverManager.getConnection(
servletContext.getInitParameter("db.url"), logger.debug("Starting plugin system...")
servletContext.getInitParameter("db.user"), plugin.PluginSystem.init()
servletContext.getInitParameter("db.password"))
scheduler.start()
} PluginUpdateJob.schedule(scheduler)
logger.debug("PluginUpdateJob is started.")
logger.debug("Plugin system is initialized.")
}
def contextDestroyed(sce: ServletContextEvent): Unit = {
scheduler.shutdown()
}
private def getConnection(servletContext: ServletContext): Connection =
DriverManager.getConnection(
servletContext.getInitParameter("db.url"),
servletContext.getInitParameter("db.user"),
servletContext.getInitParameter("db.password"))
}

View File

@@ -0,0 +1,81 @@
package servlet
import javax.servlet._
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import org.apache.commons.io.IOUtils
import twirl.api.Html
import service.{AccountService, RepositoryService, SystemSettingsService}
import model.Account
import util.{JGitUtil, Keys}
class PluginActionInvokeFilter extends Filter with SystemSettingsService with RepositoryService with AccountService {
def init(config: FilterConfig) = {}
def destroy(): Unit = {}
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
(req, res) match {
case (request: HttpServletRequest, response: HttpServletResponse) => {
Database(req.getServletContext) withTransaction { implicit session =>
val path = req.asInstanceOf[HttpServletRequest].getRequestURI
if(!processGlobalAction(path, request, response) && !processRepositoryAction(path, request, response)){
chain.doFilter(req, res)
}
}
}
}
}
private def processGlobalAction(path: String, request: HttpServletRequest, response: HttpServletResponse): Boolean = {
plugin.PluginSystem.globalActions.find(_.path == path).map { action =>
val result = action.function(request, response)
result match {
case x: String => {
response.setContentType("text/html; charset=UTF-8")
val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
implicit val context = app.Context(loadSystemSettings(), Option(loginAccount), request)
val html = _root_.html.main("GitBucket", None)(Html(x))
IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
}
case x => {
// TODO returns as JSON?
response.setContentType("application/json; charset=UTF-8")
}
}
true
} getOrElse false
}
private def processRepositoryAction(path: String, request: HttpServletRequest, response: HttpServletResponse)
(implicit session: model.simple.Session): Boolean = {
val elements = path.split("/")
if(elements.length > 3){
val owner = elements(1)
val name = elements(2)
val remain = elements.drop(3).mkString("/", "/", "")
getRepository(owner, name, "").flatMap { repository => // TODO fill baseUrl
plugin.PluginSystem.repositoryActions.find(_.path == remain).map { action =>
val result = action.function(request, response)
result match {
case x: String => {
response.setContentType("text/html; charset=UTF-8")
val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
implicit val context = app.Context(loadSystemSettings(), Option(loginAccount), request)
val html = _root_.html.main("GitBucket", None)(_root_.html.menu("", repository)(Html(x))) // TODO specify active side menu
IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
}
case x => {
// TODO returns as JSON?
response.setContentType("application/json; charset=UTF-8")
}
}
true
}
} getOrElse false
} else false
}
}

View File

@@ -33,6 +33,7 @@ class TransactionFilter extends Filter {
} }
object Database { object Database {
def apply(context: ServletContext): slick.jdbc.JdbcBackend.Database = def apply(context: ServletContext): slick.jdbc.JdbcBackend.Database =
slick.jdbc.JdbcBackend.Database.forURL(context.getInitParameter("db.url"), slick.jdbc.JdbcBackend.Database.forURL(context.getInitParameter("db.url"),
context.getInitParameter("db.user"), context.getInitParameter("db.user"),

View File

@@ -34,6 +34,10 @@ object Directory {
val DatabaseHome = s"${GitBucketHome}/data" val DatabaseHome = s"${GitBucketHome}/data"
val PluginHome = s"${GitBucketHome}/plugins"
val TemporaryHome = s"${GitBucketHome}/tmp"
/** /**
* Substance directory of the repository. * Substance directory of the repository.
*/ */
@@ -55,13 +59,18 @@ object Directory {
* Root of temporary directories for the upload file. * Root of temporary directories for the upload file.
*/ */
def getTemporaryDir(sessionId: String): File = def getTemporaryDir(sessionId: String): File =
new File(s"${GitBucketHome}/tmp/_upload/${sessionId}") new File(s"${TemporaryHome}/_upload/${sessionId}")
/** /**
* Root of temporary directories for the specified repository. * Root of temporary directories for the specified repository.
*/ */
def getTemporaryDir(owner: String, repository: String): File = def getTemporaryDir(owner: String, repository: String): File =
new File(s"${GitBucketHome}/tmp/${owner}/${repository}") new File(s"${TemporaryHome}/${owner}/${repository}")
/**
* Root of plugin cache directory. Plugin repositories are cloned into this directory.
*/
def getPluginCacheDir(): File = new File(s"${TemporaryHome}/_plugins")
/** /**
* Temporary directory which is used to create an archive to download repository contents. * Temporary directory which is used to create an archive to download repository contents.

View File

@@ -35,7 +35,11 @@ object JGitUtil {
* @param branchList the list of branch names * @param branchList the list of branch names
* @param tags the list of tags * @param tags the list of tags
*/ */
case class RepositoryInfo(owner: String, name: String, url: String, commitCount: Int, branchList: List[String], tags: List[TagInfo]) case class RepositoryInfo(owner: String, name: String, url: String, commitCount: Int, branchList: List[String], tags: List[TagInfo]){
def this(owner: String, name: String, baseUrl: String) = {
this(owner, name, s"${baseUrl}/git/${owner}/${name}.git", 0, Nil, Nil)
}
}
/** /**
* The file data for the file list of the repository viewer. * The file data for the file list of the repository viewer.

View File

@@ -47,11 +47,11 @@ object LDAPUtil {
keystore = ldapSettings.keystore.getOrElse(""), keystore = ldapSettings.keystore.getOrElse(""),
error = "User LDAP Authentication Failed." error = "User LDAP Authentication Failed."
){ conn => ){ conn =>
findMailAddress(conn, userDN, ldapSettings.mailAttribute) match { findMailAddress(conn, userDN, ldapSettings.userNameAttribute, userName, ldapSettings.mailAttribute) match {
case Some(mailAddress) => Right(LDAPUserInfo( case Some(mailAddress) => Right(LDAPUserInfo(
userName = getUserNameFromMailAddress(userName), userName = getUserNameFromMailAddress(userName),
fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute => fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute =>
findFullName(conn, userDN, fullNameAttribute) findFullName(conn, userDN, ldapSettings.userNameAttribute, userName, fullNameAttribute)
}.getOrElse(userName), }.getOrElse(userName),
mailAddress = mailAddress)) mailAddress = mailAddress))
case None => Left("Can't find mail address.") case None => Left("Can't find mail address.")
@@ -130,15 +130,15 @@ object LDAPUtil {
} }
} }
private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] = private def findMailAddress(conn: LDAPConnection, userDN: String, userNameAttribute: String, userName: String, mailAttribute: String): Option[String] =
defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false)){ results => defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, userNameAttribute + "=" + userName, Array[String](mailAttribute), false)){ results =>
if(results.hasMore) { if(results.hasMore) {
Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue) Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
} else None } else None
} }
private def findFullName(conn: LDAPConnection, userDN: String, nameAttribute: String): Option[String] = private def findFullName(conn: LDAPConnection, userDN: String, userNameAttribute: String, userName: String, nameAttribute: String): Option[String] =
defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](nameAttribute), false)){ results => defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, userNameAttribute + "=" + userName, Array[String](nameAttribute), false)){ results =>
if(results.hasMore) { if(results.hasMore) {
Option(results.next.getAttribute(nameAttribute)).map(_.getStringValue) Option(results.next.getAttribute(nameAttribute)).map(_.getStringValue)
} else None } else None

View File

@@ -11,6 +11,9 @@
<li@if(active=="system"){ class="active"}> <li@if(active=="system"){ class="active"}>
<a href="@path/admin/system">System Settings</a> <a href="@path/admin/system">System Settings</a>
</li> </li>
<li@if(active=="plugins"){ class="active"}>
<a href="@path/admin/plugins">Plugins</a>
</li>
<li> <li>
<a href="@path/console/login.jsp">H2 Console</a> <a href="@path/console/login.jsp">H2 Console</a>
</li> </li>

View File

@@ -0,0 +1,37 @@
@(plugins: List[app.SystemSettingsControllerBase.AvailablePlugin])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Plugins"){
@admin.html.menu("plugins"){
@tab("available")
<form action="@path/admin/plugins/_install" method="POST" validate="true">
<table class="table table-bordered">
<tr>
<th>ID</th>
<th>Version</th>
<th>Provider</th>
<th>Description</th>
</tr>
@plugins.zipWithIndex.map { case (plugin, i) =>
<tr>
<td>
<input type="checkbox" name="pluginId[@i]" value="@plugin.id"/>
@plugin.id
</td>
<td>@plugin.version</td>
<td><a href="@plugin.url">@plugin.author</a></td>
<td>@plugin.description</td>
</tr>
}
</table>
<input type="submit" id="install-plugins" class="btn btn-success" value="Install selected plugins"/>
</form>
}
}
<script>
$(function(){
$('#install-plugins').click(function(){
return confirm('Selected plugin will be installed. Are you sure?');
});
});
</script>

View File

@@ -0,0 +1,37 @@
@()(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("JavaScript Console"){
@admin.html.menu("plugins"){
@tab("console")
<form method="POST">
<div class="box">
<div class="box-header">JavaScript Console</div>
<div class="box-content">
<div id="editor" style="width: 100%; height: 400px;"></div>
</div>
</div>
<fieldset>
<input type="button" id="evaluate" class="btn btn-success" value="Evaluate"/>
</fieldset>
</form>
}
}
<script src="@assets/vendors/ace/ace.js" type="text/javascript" charset="utf-8"></script>
<script>
$(function(){
var editor = ace.edit("editor");
editor.setTheme("ace/theme/monokai");
editor.getSession().setMode("ace/mode/javascript");
$('#evaluate').click(function(){
$.post('@path/admin/plugins/console', {
script: editor.getValue()
}, function(data){
alert('Success: ' + data);
}).fail(function(error){
alert(error.statusText);
});
});
});
</script>

View File

@@ -0,0 +1,47 @@
@(plugins: List[plugin.Plugin],
updatablePlugins: List[app.SystemSettingsControllerBase.AvailablePlugin])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Plugins"){
@admin.html.menu("plugins"){
@tab("installed")
<form method="POST" validate="true">
<table class="table table-bordered">
<tr>
<th>ID</th>
<th>Version</th>
<th>Provider</th>
<th>Description</th>
</tr>
@plugins.zipWithIndex.map { case (plugin, i) =>
<tr>
<td>
<input type="checkbox" name="pluginId[@i]" value="@plugin.id"/>
@plugin.id
</td>
<td>
@plugin.version
@updatablePlugins.find(_.id == plugin.id).map { x =>
(@x.version is available)
}
</td>
<td><a href="@plugin.url">@plugin.author</a></td>
<td>@plugin.description</td>
</tr>
}
</table>
<input type="submit" id="update-plugins" class="btn btn-success" value="Update selected plugins" formaction="@path/admin/plugins/_update"/>
<input type="submit" id="delete-plugins" class="btn btn-danger" value="Uninstall selected plugins" formaction="@path/admin/plugins/_delete"/>
</form>
}
}
<script>
$(function(){
$('#update-plugins').click(function(){
return confirm('Selected plugin will be updated. Are you sure?');
});
$('#delete-plugins').click(function(){
return confirm('Selected plugin will be removed permanently. Are you sure?');
});
});
</script>

View File

@@ -0,0 +1,9 @@
@(active: String)(implicit context: app.Context)
@import context._
<ul class="nav nav-tabs">
<li@if(active == "installed"){ class="active"}><a href="@path/admin/plugins">Installed plugins</a></li>
<li@if(active == "available"){ class="active"}><a href="@path/admin/plugins/available">Available plugins</a></li>
@*
<li@if(active == "console" ){ class="active"}><a href="@path/admin/plugins/console">JavaScript console</a></li>
*@
</ul>

View File

@@ -1,71 +1,71 @@
@(users: List[model.Account], members: Map[String, List[String]], includeRemoved: Boolean)(implicit context: app.Context) @(users: List[model.Account], members: Map[String, List[String]], includeRemoved: Boolean)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main("Manage Users"){ @html.main("Manage Users"){
@admin.html.menu("users"){ @admin.html.menu("users"){
<div class="pull-right" style="margin-bottom: 4px;"> <div class="pull-right" style="margin-bottom: 4px;">
<a href="@path/admin/users/_newuser" class="btn">New User</a> <a href="@path/admin/users/_newuser" class="btn">New User</a>
<a href="@path/admin/users/_newgroup" class="btn">New Group</a> <a href="@path/admin/users/_newgroup" class="btn">New Group</a>
</div> </div>
<label for="includeRemoved"> <label for="includeRemoved">
<input type="checkbox" id="includeRemoved" name="includeRemoved" @if(includeRemoved){checked}/> <input type="checkbox" id="includeRemoved" name="includeRemoved" @if(includeRemoved){checked}/>
Include removed users Include removed users
</label> </label>
<table class="table table-bordered table-hover"> <table class="table table-bordered table-hover">
@users.map { account => @users.map { account =>
<tr> <tr>
<td @if(account.isRemoved){style="background-color: #dddddd;"}> <td @if(account.isRemoved){style="background-color: #dddddd;"}>
<div class="pull-right"> <div class="pull-right">
@if(account.isGroupAccount){ @if(account.isGroupAccount){
<a href="@path/admin/users/@account.userName/_editgroup">Edit</a> <a href="@path/admin/users/@account.userName/_editgroup">Edit</a>
} else { } else {
<a href="@path/admin/users/@account.userName/_edituser">Edit</a> <a href="@path/admin/users/@account.userName/_edituser">Edit</a>
} }
</div> </div>
<div class="strong"> <div class="strong">
@avatar(account.userName, 20) @avatar(account.userName, 20)
<a href="@url(account.userName)">@account.userName</a> <a href="@url(account.userName)">@account.userName</a>
@if(account.isGroupAccount){ @if(account.isGroupAccount){
(Group) (Group)
} else { } else {
@if(account.isAdmin){ @if(account.isAdmin){
(Administrator) (Administrator)
} else { } else {
(Normal) (Normal)
} }
} }
@if(account.isGroupAccount){ @if(account.isGroupAccount){
@members(account.userName).map { userName => @members(account.userName).map { userName =>
@avatar(userName, 20, tooltip = true) @avatar(userName, 20, tooltip = true)
} }
} }
</div> </div>
<div> <div>
<hr> <hr>
@if(!account.isGroupAccount){ @if(!account.isGroupAccount){
<i class="icon-envelope"></i> @account.mailAddress <i class="icon-envelope"></i> @account.mailAddress
} }
@account.url.map { url => @account.url.map { url =>
<i class="icon-home"></i> @url <i class="icon-home"></i> @url
} }
</div> </div>
<div> <div>
<span class="muted">Registered:</span> @datetime(account.registeredDate) <span class="muted">Registered:</span> @datetime(account.registeredDate)
<span class="muted">Updated:</span> @datetime(account.updatedDate) <span class="muted">Updated:</span> @datetime(account.updatedDate)
@if(!account.isGroupAccount){ @if(!account.isGroupAccount){
<span class="muted">Last Login:</span> @account.lastLoginDate.map(datetime) <span class="muted">Last Login:</span> @account.lastLoginDate.map(datetime)
} }
</div> </div>
</td> </td>
</tr> </tr>
} }
</table> </table>
} }
} }
<script> <script>
$(function(){ $(function(){
$('#includeRemoved').click(function(){ $('#includeRemoved').click(function(){
location.href = '@path/admin/users?includeRemoved=' + this.checked; location.href = '@path/admin/users?includeRemoved=' + this.checked;
}); });
}); });
</script> </script>

View File

@@ -1,80 +1,80 @@
@(account: Option[model.Account])(implicit context: app.Context) @(account: Option[model.Account])(implicit context: app.Context)
@import context._ @import context._
@html.main(if(account.isEmpty) "New User" else "Update User"){ @html.main(if(account.isEmpty) "New User" else "Update User"){
@admin.html.menu("users"){ @admin.html.menu("users"){
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_newuser} else {@path/admin/users/@account.get.userName/_edituser}" validate="true"> <form method="POST" action="@if(account.isEmpty){@path/admin/users/_newuser} else {@path/admin/users/@account.get.userName/_edituser}" validate="true">
<div class="row-fluid"> <div class="row-fluid">
<div class="span6"> <div class="span6">
<fieldset> <fieldset>
<label for="userName" class="strong">Username:</label> <label for="userName" class="strong">Username:</label>
<div> <div>
<span id="error-userName" class="error"></span> <span id="error-userName" class="error"></span>
</div> </div>
<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}/>
@if(account.isDefined){ @if(account.isDefined){
<label for="removed"> <label for="removed">
<input type="checkbox" name="removed" id="removed" value="true" @if(account.get.isRemoved){checked}/> <input type="checkbox" name="removed" id="removed" value="true" @if(account.get.isRemoved){checked}/>
Disable Disable
</label> </label>
} }
</fieldset> </fieldset>
@if(account.map(_.password.nonEmpty).getOrElse(true)){ @if(account.map(_.password.nonEmpty).getOrElse(true)){
<fieldset> <fieldset>
<label for="password" class="strong"> <label for="password" class="strong">
Password Password
@if(account.isDefined){ @if(account.isDefined){
(Input to change password) (Input to change password)
} }
: :
</label> </label>
<div> <div>
<span id="error-password" class="error"></span> <span id="error-password" class="error"></span>
</div> </div>
<input type="password" name="password" id="password" value="" autocomplete="off"/> <input type="password" name="password" id="password" value="" autocomplete="off"/>
</fieldset> </fieldset>
} }
<fieldset> <fieldset>
<label for="fullName" class="strong">Full Name:</label> <label for="fullName" class="strong">Full Name:</label>
<div> <div>
<span id="error-fullName" class="error"></span> <span id="error-fullName" class="error"></span>
</div> </div>
<input type="text" name="fullName" id="fullName" value="@account.map(_.fullName)"/> <input type="text" name="fullName" id="fullName" value="@account.map(_.fullName)"/>
</fieldset> </fieldset>
<fieldset> <fieldset>
<label for="mailAddress" class="strong">Mail Address:</label> <label for="mailAddress" class="strong">Mail Address:</label>
<div> <div>
<span id="error-mailAddress" class="error"></span> <span id="error-mailAddress" class="error"></span>
</div> </div>
<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 class="strong">User Type:</label> <label class="strong">User Type:</label>
<label class="radio" for="userType_Normal"> <label class="radio" 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>
<label class="radio" for="userType_Admin"> <label class="radio" for="userType_Admin">
<input type="radio" name="isAdmin" id="userType_Admin" value="true"@if(account.isDefined && account.get.isAdmin){ checked}/> Administrator <input type="radio" name="isAdmin" id="userType_Admin" value="true"@if(account.isDefined && account.get.isAdmin){ checked}/> Administrator
</label> </label>
</fieldset> </fieldset>
<fieldset> <fieldset>
<label class="strong">URL (Optional):</label> <label class="strong">URL (Optional):</label>
<div> <div>
<span id="error-url" class="error"></span> <span id="error-url" class="error"></span>
</div> </div>
<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" class="strong">Image (Optional)</label> <label for="avatar" class="strong">Image (Optional)</label>
@helper.html.uploadavatar(account) @helper.html.uploadavatar(account)
</fieldset> </fieldset>
</div> </div>
</div> </div>
<fieldset class="margin"> <fieldset class="margin">
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create User} else {Update User}"/> <input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create User} else {Update User}"/>
<a href="@path/admin/users" class="btn">Cancel</a> <a href="@path/admin/users" class="btn">Cancel</a>
</fieldset> </fieldset>
</form> </form>
} }
} }

View File

@@ -1,98 +1,98 @@
@(activities: List[model.Activity])(implicit context: app.Context) @(activities: List[model.Activity])(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@if(activities.isEmpty){ @if(activities.isEmpty){
No activity No activity
} else { } else {
@activities.map { activity => @activities.map { activity =>
<div class="block"> <div class="block">
@(activity.activityType match { @(activity.activityType match {
case "open_issue" => detailActivity(activity, "activity-issue.png") case "open_issue" => detailActivity(activity, "activity-issue.png")
case "comment_issue" => detailActivity(activity, "activity-comment.png") case "comment_issue" => detailActivity(activity, "activity-comment.png")
case "close_issue" => detailActivity(activity, "activity-issue-close.png") case "close_issue" => detailActivity(activity, "activity-issue-close.png")
case "reopen_issue" => detailActivity(activity, "activity-issue-reopen.png") case "reopen_issue" => detailActivity(activity, "activity-issue-reopen.png")
case "open_pullreq" => detailActivity(activity, "activity-merge.png") case "open_pullreq" => detailActivity(activity, "activity-merge.png")
case "merge_pullreq" => detailActivity(activity, "activity-merge.png") case "merge_pullreq" => detailActivity(activity, "activity-merge.png")
case "create_repository" => simpleActivity(activity, "activity-create-repository.png") case "create_repository" => simpleActivity(activity, "activity-create-repository.png")
case "create_branch" => simpleActivity(activity, "activity-branch.png") case "create_branch" => simpleActivity(activity, "activity-branch.png")
case "delete_branch" => simpleActivity(activity, "activity-delete.png") case "delete_branch" => simpleActivity(activity, "activity-delete.png")
case "create_tag" => simpleActivity(activity, "activity-tag.png") case "create_tag" => simpleActivity(activity, "activity-tag.png")
case "delete_tag" => simpleActivity(activity, "activity-delete.png") case "delete_tag" => simpleActivity(activity, "activity-delete.png")
case "fork" => simpleActivity(activity, "activity-fork.png") case "fork" => simpleActivity(activity, "activity-fork.png")
case "push" => customActivity(activity, "activity-commit.png"){ case "push" => customActivity(activity, "activity-commit.png"){
<div class="small activity-message"> <div class="small activity-message">
{activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) => {activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) =>
if(i == 3){ if(i == 3){
<div>...</div> <div>...</div>
} else { } else {
if(commit.nonEmpty){ if(commit.nonEmpty){
<div> <div>
<a href={s"${path}/${activity.userName}/${activity.repositoryName}/commit/${commit. substring(0, 40)}"} class="monospace">{commit.substring(0, 7)}</a> <a href={s"${path}/${activity.userName}/${activity.repositoryName}/commit/${commit. substring(0, 40)}"} class="monospace">{commit.substring(0, 7)}</a>
<span>{commit.substring(41)}</span> <span>{commit.substring(41)}</span>
</div> </div>
} }
} }
}} }}
</div> </div>
} }
case "create_wiki" => customActivity(activity, "activity-wiki.png"){ case "create_wiki" => customActivity(activity, "activity-wiki.png"){
<div class="small activity-message"> <div class="small activity-message">
Created <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${activity.additionalInfo.get}"}>{activity.additionalInfo.get}</a>. Created <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${activity.additionalInfo.get}"}>{activity.additionalInfo.get}</a>.
</div> </div>
} }
case "edit_wiki" => customActivity(activity, "activity-wiki.png"){ case "edit_wiki" => customActivity(activity, "activity-wiki.png"){
activity.additionalInfo.get.split(":") match { activity.additionalInfo.get.split(":") match {
case Array(pageName, commitId) => case Array(pageName, commitId) =>
<div class="small activity-message"> <div class="small activity-message">
Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}"}>{pageName}</a>. Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}"}>{pageName}</a>.
<a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}/_compare/${commitId.substring(0, 7)}^...${commitId.substring(0, 7)}"}>View the diff »</a> <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}/_compare/${commitId.substring(0, 7)}^...${commitId.substring(0, 7)}"}>View the diff »</a>
</div> </div>
case Array(pageName) => case Array(pageName) =>
<div class="small activity-message"> <div class="small activity-message">
Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}"}>{pageName}</a>. Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}"}>{pageName}</a>.
</div> </div>
} }
} }
}) })
</div> </div>
} }
} }
@detailActivity(activity: model.Activity, image: String) = { @detailActivity(activity: model.Activity, image: String) = {
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div> <div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
<div class="activity-content"> <div class="activity-content">
<div class="muted small">@datetime(activity.activityDate)</div> <div class="muted small">@datetime(activity.activityDate)</div>
<div class="strong"> <div class="strong">
@avatar(activity.activityUserName, 16) @avatar(activity.activityUserName, 16)
@activityMessage(activity.message) @activityMessage(activity.message)
</div> </div>
@activity.additionalInfo.map { additionalInfo => @activity.additionalInfo.map { additionalInfo =>
<div class=" activity-message">@additionalInfo</div> <div class=" activity-message">@additionalInfo</div>
} }
</div> </div>
} }
@customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = { @customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = {
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div> <div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
<div class="activity-content"> <div class="activity-content">
<div class="muted small">@datetime(activity.activityDate)</div> <div class="muted small">@datetime(activity.activityDate)</div>
<div class="strong"> <div class="strong">
@avatar(activity.activityUserName, 16) @avatar(activity.activityUserName, 16)
@activityMessage(activity.message) @activityMessage(activity.message)
</div> </div>
@additionalInfo @additionalInfo
</div> </div>
} }
@simpleActivity(activity: model.Activity, image: String) = { @simpleActivity(activity: model.Activity, image: String) = {
<div class="activity-icon-small"><img src="@assets/common/images/@image"/></div> <div class="activity-icon-small"><img src="@assets/common/images/@image"/></div>
<div class="activity-content"> <div class="activity-content">
<div> <div>
@avatar(activity.activityUserName, 16) @avatar(activity.activityUserName, 16)
@activityMessage(activity.message) @activityMessage(activity.message)
<span class="muted small">@datetime(activity.activityDate)</span> <span class="muted small">@datetime(activity.activityDate)</span>
</div> </div>
</div> </div>
} }

View File

@@ -1,105 +1,105 @@
@(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], oldCommitId: Option[String],
showIndex: Boolean)(implicit context: app.Context) 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){ @if(showIndex){
<div> <div>
<div class="pull-right" style="margin-bottom: 10px;"> <div class="pull-right" style="margin-bottom: 10px;">
<input id="toggle-file-list" type="button" class="btn" value="Show file list"/> <input id="toggle-file-list" type="button" class="btn" value="Show file list"/>
</div> </div>
Showing @diffs.size changed @plural(diffs.size, "file") Showing @diffs.size changed @plural(diffs.size, "file")
</div> </div>
<ul id="commit-file-list" style="display: none;"> <ul id="commit-file-list" style="display: none;">
@diffs.zipWithIndex.map { case (diff, i) => @diffs.zipWithIndex.map { case (diff, i) =>
<li@if(i > 0){ class="border"}> <li@if(i > 0){ class="border"}>
<a href="#diff-@i"> <a href="#diff-@i">
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){ @if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
<img src="@assets/common/images/diff_move.png"/> @diff.oldPath -> @diff.newPath <img src="@assets/common/images/diff_move.png"/> @diff.oldPath -> @diff.newPath
} }
@if(diff.changeType == ChangeType.ADD){ @if(diff.changeType == ChangeType.ADD){
<img src="@assets/common/images/diff_add.png"/> @diff.newPath <img src="@assets/common/images/diff_add.png"/> @diff.newPath
} }
@if(diff.changeType == ChangeType.MODIFY){ @if(diff.changeType == ChangeType.MODIFY){
<img src="@assets/common/images/diff_edit.png"/> @diff.newPath <img src="@assets/common/images/diff_edit.png"/> @diff.newPath
} }
@if(diff.changeType == ChangeType.DELETE){ @if(diff.changeType == ChangeType.DELETE){
<img src="@assets/common/images/diff_delete.png"/> @diff.oldPath <img src="@assets/common/images/diff_delete.png"/> @diff.oldPath
} }
</a> </a>
</li> </li>
} }
</ul> </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">
<tr> <tr>
<th style="font-weight: normal;" class="box-header"> <th style="font-weight: normal;" class="box-header">
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){ @if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
@diff.oldPath -> @diff.newPath @diff.oldPath -> @diff.newPath
@if(newCommitId.isDefined){ @if(newCommitId.isDefined){
<div class="pull-right align-right"> <div class="pull-right align-right">
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-small">View file @@ @newCommitId.get.substring(0, 10)</a> <a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-small">View file @@ @newCommitId.get.substring(0, 10)</a>
</div> </div>
} }
} }
@if(diff.changeType == ChangeType.ADD || diff.changeType == ChangeType.MODIFY){ @if(diff.changeType == ChangeType.ADD || diff.changeType == ChangeType.MODIFY){
@diff.newPath @diff.newPath
@if(newCommitId.isDefined){ @if(newCommitId.isDefined){
<div class="pull-right align-right"> <div class="pull-right align-right">
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-small">View file @@ @newCommitId.get.substring(0, 10)</a> <a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-small">View file @@ @newCommitId.get.substring(0, 10)</a>
</div> </div>
} }
} }
@if(diff.changeType == ChangeType.DELETE){ @if(diff.changeType == ChangeType.DELETE){
@diff.oldPath @diff.oldPath
@if(oldCommitId.isDefined){ @if(oldCommitId.isDefined){
<div class="pull-right align-right"> <div class="pull-right align-right">
<a href="@url(repository)/blob/@oldCommitId.get/@diff.oldPath" class="btn btn-small">View file @@ @oldCommitId.get.substring(0, 10)</a> <a href="@url(repository)/blob/@oldCommitId.get/@diff.oldPath" class="btn btn-small">View file @@ @oldCommitId.get.substring(0, 10)</a>
</div> </div>
} }
} }
</th> </th>
</tr> </tr>
<tr> <tr>
<td> <td>
@if(diff.newContent != None || diff.oldContent != None){ @if(diff.newContent != None || diff.oldContent != None){
<div id="diffText-@i"></div> <div id="diffText-@i"></div>
<textarea id="newText-@i" style="display: none;">@diff.newContent.getOrElse("")</textarea> <textarea id="newText-@i" style="display: none;">@diff.newContent.getOrElse("")</textarea>
<textarea id="oldText-@i" style="display: none;">@diff.oldContent.getOrElse("")</textarea> <textarea id="oldText-@i" style="display: none;">@diff.oldContent.getOrElse("")</textarea>
} else { } else {
Not supported Not supported
} }
</td> </td>
</tr> </tr>
</table> </table>
} }
<script type="text/javascript" src="@assets/jsdifflib/difflib.js"></script> <script type="text/javascript" src="@assets/vendors/jsdifflib/difflib.js"></script>
<script type="text/javascript" src="@assets/jsdifflib/diffview.js"></script> <script type="text/javascript" src="@assets/vendors/jsdifflib/diffview.js"></script>
<link href="@assets/jsdifflib/diffview.css" type="text/css" rel="stylesheet" /> <link href="@assets/vendors/jsdifflib/diffview.css" type="text/css" rel="stylesheet" />
<script> <script>
$(function(){ $(function(){
@if(showIndex){ @if(showIndex){
$('#toggle-file-list').click(function(){ $('#toggle-file-list').click(function(){
$('#commit-file-list').toggle(); $('#commit-file-list').toggle();
if($(this).val() == 'Show file list'){ if($(this).val() == 'Show file list'){
$(this).val('Hide file list'); $(this).val('Hide file list');
} else { } else {
$(this).val('Show file list'); $(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){
diffUsingJS('oldText-@i', 'newText-@i', 'diffText-@i'); diffUsingJS('oldText-@i', 'newText-@i', 'diffText-@i');
} }
} }
} }
}); });
</script> </script>

View File

@@ -25,8 +25,8 @@
</div> </div>
</div> </div>
</div> </div>
<link href="@assets/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/> <link href="@assets/vendors/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/>
<script src="@assets/google-code-prettify/prettify.js"></script> <script src="@assets/vendors/google-code-prettify/prettify.js"></script>
<script> <script>
$(function(){ $(function(){
@if(elastic){ @if(elastic){

View File

@@ -1,70 +1,70 @@
@(activities: List[model.Activity], @(activities: List[model.Activity],
recentRepositories: List[service.RepositoryService.RepositoryInfo], recentRepositories: List[service.RepositoryService.RepositoryInfo],
userRepositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context) userRepositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@main("GitBucket"){ @main("GitBucket"){
<div class="container"> <div class="container">
@dashboard.html.tab() @dashboard.html.tab()
<div class="row-fluid"> <div class="row-fluid">
<div class="span8"> <div class="span8">
@helper.html.activities(activities) @helper.html.activities(activities)
</div> </div>
<div class="span4"> <div class="span4">
@if(loginAccount.isEmpty){ @if(loginAccount.isEmpty){
@signinform(settings) @signinform(settings)
} else { } else {
<table class="table table-bordered"> <table class="table table-bordered">
<tr> <tr>
<th class="metal"> <th class="metal">
<div class="pull-right"> <div class="pull-right">
<a href="@path/new" class="btn btn-success btn-mini">New repository</a> <a href="@path/new" class="btn btn-success btn-mini">New repository</a>
</div> </div>
Your repositories (@userRepositories.size) Your repositories (@userRepositories.size)
</th> </th>
</tr> </tr>
@if(userRepositories.isEmpty){ @if(userRepositories.isEmpty){
<tr> <tr>
<td>No repositories</td> <td>No repositories</td>
</tr> </tr>
} else { } else {
@userRepositories.map { repository => @userRepositories.map { repository =>
<tr> <tr>
<td> <td>
@helper.html.repositoryicon(repository, false) @helper.html.repositoryicon(repository, false)
@if(repository.owner == loginAccount.get.userName){ @if(repository.owner == loginAccount.get.userName){
<a href="@url(repository)"><span class="strong">@repository.name</span></a> <a href="@url(repository)"><span class="strong">@repository.name</span></a>
} else { } else {
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a> <a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
} }
</td> </td>
</tr> </tr>
} }
} }
</table> </table>
} }
<table class="table table-bordered"> <table class="table table-bordered">
<tr> <tr>
<th class="metal"> <th class="metal">
Recent updated repositories Recent updated repositories
</th> </th>
</tr> </tr>
@if(recentRepositories.isEmpty){ @if(recentRepositories.isEmpty){
<tr> <tr>
<td>No repositories</td> <td>No repositories</td>
</tr> </tr>
} else { } else {
@recentRepositories.map { repository => @recentRepositories.map { repository =>
<tr> <tr>
<td> <td>
@helper.html.repositoryicon(repository, false) @helper.html.repositoryicon(repository, false)
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a> <a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
</td> </td>
</tr> </tr>
} }
} }
</table> </table>
</div> </div>
</div> </div>
</div> </div>
} }

View File

@@ -1,147 +1,147 @@
@(collaborators: List[String], @(collaborators: List[String],
milestones: List[model.Milestone], milestones: List[model.Milestone],
labels: List[model.Label], labels: List[model.Label],
hasWritePermission: Boolean, hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){ @html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("issues", repository){ @html.menu("issues", repository){
@tab("", true, repository) @tab("", true, repository)
<form action="@url(repository)/issues/new" method="POST" validate="true"> <form action="@url(repository)/issues/new" method="POST" validate="true">
<div class="row-fluid"> <div class="row-fluid">
<div class="span9"> <div class="span9">
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div> <div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
<div class="box issue-box"> <div class="box issue-box">
<div class="box-content"> <div class="box-content">
<span id="error-title" class="error"></span> <span id="error-title" class="error"></span>
<input type="text" name="title" value="" placeholder="Title" style="width: 565px;"/> <input type="text" name="title" value="" placeholder="Title" style="width: 565px;"/>
<div> <div>
<span id="label-assigned">No one is assigned</span> <span id="label-assigned">No one is assigned</span>
@if(hasWritePermission){ @if(hasWritePermission){
<input type="hidden" name="assignedUserName" value=""/> <input type="hidden" name="assignedUserName" value=""/>
@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-while"></i>@avatar(collaborator, 20) @collaborator</a></li> <li><a href="javascript:void(0);" class="assign" data-name="@collaborator"><i class="icon-while"></i>@avatar(collaborator, 20) @collaborator</a></li>
} }
} }
} }
<div class="pull-right"> <div class="pull-right">
<span id="label-milestone">No milestone</span> <span id="label-milestone">No milestone</span>
@if(hasWritePermission){ @if(hasWritePermission){
<input type="hidden" name="milestoneId" value=""/> <input type="hidden" name="milestoneId" value=""/>
@helper.html.dropdown() { @helper.html.dropdown() {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li> <li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li>
@milestones.filter(_.closedDate.isEmpty).map { milestone => @milestones.filter(_.closedDate.isEmpty).map { 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-while"></i> @milestone.title <i class="icon-while"></i> @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)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate) <img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else { } else {
<span class="muted">Due in @date(dueDate)</span> <span class="muted">Due in @date(dueDate)</span>
} }
}.getOrElse { }.getOrElse {
<span class="muted">No due date</span> <span class="muted">No due date</span>
} }
</div> </div>
</a> </a>
</li> </li>
} }
} }
} }
</div> </div>
</div> </div>
<hr> <hr>
@helper.html.preview(repository, "", false, true, "width: 565px; height: 200px; max-height: 250px;", elastic = true) @helper.html.preview(repository, "", false, true, "width: 565px; height: 200px; max-height: 250px;", elastic = true)
</div> </div>
</div> </div>
<div class="pull-right"> <div class="pull-right">
<input type="submit" class="btn btn-success" value="Submit new issue"/> <input type="submit" class="btn btn-success" value="Submit new issue"/>
</div> </div>
</div> </div>
<div class="span3"> <div class="span3">
@if(hasWritePermission){ @if(hasWritePermission){
<span class="strong">Add Labels</span> <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">
@labels.map { label => @labels.map { label =>
<li> <li>
<a href="javascript:void(0);" class="toggle-label" data-label="@label.labelName" data-bgcolor="@label.color" data-fgcolor="@label.fontColor"> <a href="javascript:void(0);" class="toggle-label" data-label="@label.labelName" data-bgcolor="@label.color" data-fgcolor="@label.fontColor">
<span style="background-color: #@label.color;" class="label-color">&nbsp;&nbsp;</span> <span style="background-color: #@label.color;" class="label-color">&nbsp;&nbsp;</span>
@label.labelName @label.labelName
</a> </a>
</li> </li>
} }
</ul> </ul>
<input type="hidden" name="labelNames" value=""/> <input type="hidden" name="labelNames" value=""/>
</div> </div>
</div> </div>
} }
</div> </div>
</div> </div>
</form> </form>
} }
} }
<script> <script>
$(function(){ $(function(){
$('a.assign').click(function(){ $('a.assign').click(function(){
var userName = $(this).data('name'); var userName = $(this).data('name');
$('a.assign i.icon-ok').attr('class', 'icon-white'); $('a.assign i.icon-ok').attr('class', 'icon-white');
if(userName == ''){ if(userName == ''){
$('#label-assigned').text('No one will be assigned'); $('#label-assigned').text('No one will be assigned');
} else { } else {
$('#label-assigned').html($('<span>') $('#label-assigned').html($('<span>')
.append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName)) .append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName))
.append(' will be assigned')); .append(' will be assigned'));
$('a.assign[data-name=' + userName + '] i').attr('class', 'icon-ok'); $('a.assign[data-name=' + jqSelectorEscape(userName) + '] i').attr('class', 'icon-ok');
} }
$('input[name=assignedUserName]').val(userName); $('input[name=assignedUserName]').val(userName);
}); });
$('a.milestone').click(function(){ $('a.milestone').click(function(){
var title = $(this).data('title'); var title = $(this).data('title');
var milestoneId = $(this).data('id'); var milestoneId = $(this).data('id');
$('a.milestone i.icon-ok').attr('class', 'icon-white'); $('a.milestone i.icon-ok').attr('class', 'icon-white');
if(milestoneId == ''){ if(milestoneId == ''){
$('#label-milestone').text('No milestone'); $('#label-milestone').text('No milestone');
} else { } else {
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<span class="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);
}); });
$('a.toggle-label').click(function(){ $('a.toggle-label').click(function(){
if($(this).data('selected') == true){ if($(this).data('selected') == true){
$(this).css({ $(this).css({
'background-color': 'white', 'background-color': 'white',
'color' : 'black', 'color' : 'black',
'font-weight' : 'normal' 'font-weight' : 'normal'
}); });
$(this).data('selected', false); $(this).data('selected', false);
} else { } else {
$(this).css({ $(this).css({
'background-color': '#' + $(this).data('bgcolor'), 'background-color': '#' + $(this).data('bgcolor'),
'color' : '#' + $(this).data('fgcolor'), 'color' : '#' + $(this).data('fgcolor'),
'font-weight' : 'bold' 'font-weight' : 'bold'
}); });
$(this).data('selected', true); $(this).data('selected', true);
} }
var labelNames = Array(); var labelNames = Array();
$('a.toggle-label').each(function(i, e){ $('a.toggle-label').each(function(i, e){
if($(e).data('selected') == true){ if($(e).data('selected') == true){
labelNames.push($(e).data('label')); labelNames.push($(e).data('label'));
} }
}); });
$('input[name=labelNames]').val(labelNames.join(',')); $('input[name=labelNames]').val(labelNames.join(','));
}); });
}); });
</script> </script>

View File

@@ -116,7 +116,7 @@ $(function(){
.append($this.find('img.avatar').clone(false)).append(' ') .append($this.find('img.avatar').clone(false)).append(' ')
.append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName)) .append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName))
.append(' is assigned'); .append(' is assigned');
$('a.assign[data-name=' + userName + '] i').attr('class', 'icon-ok'); $('a.assign[data-name=' + jqSelectorEscape(userName) + '] i').attr('class', 'icon-ok');
} }
}); });
}); });

View File

@@ -9,26 +9,26 @@
<link rel="icon" href="@assets/common/images/favicon.png" type="image/vnd.microsoft.icon" /> <link rel="icon" href="@assets/common/images/favicon.png" type="image/vnd.microsoft.icon" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Le styles --> <!-- Le styles -->
<link href="@assets/bootstrap/css/bootstrap.css" rel="stylesheet"> <link href="@assets/vendors/bootstrap/css/bootstrap.css" rel="stylesheet">
<link href="@assets/bootstrap/css/bootstrap-responsive.css" rel="stylesheet"> <link href="@assets/vendors/bootstrap/css/bootstrap-responsive.css" rel="stylesheet">
<!-- HTML5 shim, for IE6-8 support of HTML5 elements --> <!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="@assets/bootstrap/js/html5shiv.js"></script> <script src="@assets/vendors/bootstrap/js/html5shiv.js"></script>
<![endif]--> <![endif]-->
<link href="@assets/datepicker/css/datepicker.css" rel="stylesheet"> <link href="@assets/vendors/datepicker/css/datepicker.css" rel="stylesheet">
<link href="@assets/colorpicker/css/bootstrap-colorpicker.css" rel="stylesheet"> <link href="@assets/vendors/colorpicker/css/bootstrap-colorpicker.css" rel="stylesheet">
<link href="@assets/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/> <link href="@assets/vendors/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/>
<link href="@assets/common/css/gitbucket.css" rel="stylesheet"> <link href="@assets/common/css/gitbucket.css" rel="stylesheet">
<script src="@assets/common/js/jquery-1.9.1.js"></script> <script src="@assets/vendors/jquery/jquery-1.9.1.js"></script>
<script src="@assets/common/js/dropzone.js"></script> <script src="@assets/vendors/dropzone/dropzone.js"></script>
<script src="@assets/common/js/validation.js"></script> <script src="@assets/common/js/validation.js"></script>
<script src="@assets/common/js/gitbucket.js"></script> <script src="@assets/common/js/gitbucket.js"></script>
<script src="@assets/bootstrap/js/bootstrap.js"></script> <script src="@assets/vendors/bootstrap/js/bootstrap.js"></script>
<script src="@assets/datepicker/js/bootstrap-datepicker.js"></script> <script src="@assets/vendors/datepicker/js/bootstrap-datepicker.js"></script>
<script src="@assets/colorpicker/js/bootstrap-colorpicker.js"></script> <script src="@assets/vendors/colorpicker/js/bootstrap-colorpicker.js"></script>
<script src="@assets/google-code-prettify/prettify.js"></script> <script src="@assets/vendors/google-code-prettify/prettify.js"></script>
<script src="@assets/zclip/ZeroClipboard.min.js"></script> <script src="@assets/vendors/zclip/ZeroClipboard.min.js"></script>
<script src="@assets/elastic/jquery.elastic.source.js"></script> <script src="@assets/vendors/elastic/jquery.elastic.source.js"></script>
</head> </head>
<body> <body>
<form id="search" action="@path/search" method="POST"> <form id="search" action="@path/search" method="POST">
@@ -60,11 +60,21 @@
<li><a href="@path/groups/new">New group</a></li> <li><a href="@path/groups/new">New group</a></li>
</ul> </ul>
<a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user"></i></a> <a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user"></i></a>
@plugin.PluginSystem.globalMenus.map { menu =>
@if(menu.condition(context)){
<a href="@menu.url" class="menu" data-toggle="tooltip" data-placement="bottom" title="@menu.label">@if(menu.icon.nonEmpty){<img src="@menu.icon" class="plugin-global-menu"/>} else {@menu.label}</a>
}
}
@if(loginAccount.get.isAdmin){ @if(loginAccount.get.isAdmin){
<a href="@path/admin/users" class="menu" data-toggle="tooltip" data-placement="bottom" title="Administration"><i class="icon-wrench"></i></a> <a href="@path/admin/users" class="menu" data-toggle="tooltip" data-placement="bottom" title="Administration"><i class="icon-wrench"></i></a>
} }
<a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a> <a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a>
} else { } else {
@plugin.PluginSystem.globalMenus.map { menu =>
@if(menu.condition(context)){
<a href="@menu.url" class="menu" data-toggle="tooltip" data-placement="bottom" title="@menu.label">@if(menu.icon.nonEmpty){<img src="@menu.icon" class="plugin-global-menu"/>} else {@menu.label}</a>
}
}
<a href="@path/signin?redirect=@urlEncode(currentPath)" class="btn btn-last" id="signin">Sign in</a> <a href="@path/signin?redirect=@urlEncode(currentPath)" class="btn btn-last" id="signin">Sign in</a>
} }
</div><!--/.nav-collapse --> </div><!--/.nav-collapse -->

View File

@@ -23,6 +23,13 @@
</li> </li>
} }
@sidemenuPlugin(path: String, name: String, label: String, icon: String) = {
<li @if(active == name){class="active"}>
<div class="@if(active == name){margin} else {gradient} pull-left"></div>
<a href="@url(repository)@path"><img src="@icon"/>@if(expand){ @label}</a>
</li>
}
<div class="container"> <div class="container">
@if(repository.commitCount > 0){ @if(repository.commitCount > 0){
<div class="pull-right"> <div class="pull-right">
@@ -54,6 +61,11 @@
@sidemenu("/issues", "issues", "Issues", repository.issueCount) @sidemenu("/issues", "issues", "Issues", repository.issueCount)
@sidemenu("/pulls" , "pulls" , "Pull Requests", repository.pullCount) @sidemenu("/pulls" , "pulls" , "Pull Requests", repository.pullCount)
@sidemenu("/wiki" , "wiki" , "Wiki") @sidemenu("/wiki" , "wiki" , "Wiki")
@plugin.PluginSystem.repositoryMenus.map { menu =>
@if(menu.condition(context)){
@sidemenuPlugin(menu.url, menu.label, menu.label, menu.icon)
}
}
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){ @if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){
@sidemenu("/settings", "settings", "Settings") @sidemenu("/settings", "settings", "Settings")
} }

View File

@@ -1,25 +1,25 @@
@(commits: Seq[Seq[util.JGitUtil.CommitInfo]], @(commits: Seq[Seq[util.JGitUtil.CommitInfo]],
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._
<div class="box"> <div class="box">
<table class="table table-file-list" style="border: 1px solid silver;"> <table class="table table-file-list" style="border: 1px solid silver;">
@commits.map { day => @commits.map { day =>
<tr> <tr>
<th colspan="3" class="box-header" style="font-weight: normal;">@date(day.head.time)</th> <th colspan="3" class="box-header" style="font-weight: normal;">@date(day.head.time)</th>
</tr> </tr>
@day.map { commit => @day.map { commit =>
<tr> <tr>
<td style="width: 20%;"> <td style="width: 20%;">
@avatar(commit, 20) @avatar(commit, 20)
@user(commit.committer, commit.mailAddress, "username") @user(commit.committer, commit.mailAddress, "username")
</td> </td>
<td>@commit.shortMessage</td> <td>@commit.shortMessage</td>
<td style="width: 10%; text-align: right;"> <td style="width: 10%; text-align: right;">
<a href="@url(repository)/commit/@commit.id" class="monospace">@commit.id.substring(0, 7)</a> <a href="@url(repository)/commit/@commit.id" class="monospace">@commit.id.substring(0, 7)</a>
</td> </td>
</tr> </tr>
} }
} }
</table> </table>
</div> </div>

View File

@@ -77,7 +77,7 @@
</table> </table>
} }
} }
<script src="@assets/common/js/jquery.ba-hashchange.js"></script> <script src="@assets/vendors/jquery/jquery.ba-hashchange.js"></script>
<script> <script>
$(window).load(function(){ $(window).load(function(){
$(window).hashchange(function(){ $(window).hashchange(function(){

View File

@@ -51,9 +51,9 @@
</form> </form>
} }
} }
<script type="text/javascript" src="@assets/jsdifflib/difflib.js"></script> <script type="text/javascript" src="@assets/vendors/jsdifflib/difflib.js"></script>
<script type="text/javascript" src="@assets/jsdifflib/diffview.js"></script> <script type="text/javascript" src="@assets/vendors/jsdifflib/diffview.js"></script>
<link href="@assets/jsdifflib/diffview.css" type="text/css" rel="stylesheet" /> <link href="@assets/vendors/jsdifflib/diffview.css" type="text/css" rel="stylesheet" />
<script> <script>
$(function(){ $(function(){
diffUsingJS('oldText', 'newText', 'diffText'); diffUsingJS('oldText', 'newText', 'diffText');

View File

@@ -68,10 +68,10 @@
</form> </form>
} }
} }
<script src="@assets/ace/ace.js" type="text/javascript" charset="utf-8"></script> <script src="@assets/vendors/ace/ace.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript" src="@assets/jsdifflib/difflib.js"></script> <script type="text/javascript" src="@assets/vendors/jsdifflib/difflib.js"></script>
<script type="text/javascript" src="@assets/jsdifflib/diffview.js"></script> <script type="text/javascript" src="@assets/vendors/jsdifflib/diffview.js"></script>
<link href="@assets/jsdifflib/diffview.css" type="text/css" rel="stylesheet" /> <link href="@assets/vendors/jsdifflib/diffview.css" type="text/css" rel="stylesheet" />
<script> <script>
$(function(){ $(function(){
$('#editor').text($('#initial').val()); $('#editor').text($('#initial').val());

View File

@@ -1,24 +1,24 @@
@(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._
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { @html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.menu("code", repository){ @html.menu("code", repository){
<h1>Tags</h1> <h1>Tags</h1>
<table class="table table-bordered"> <table class="table table-bordered">
<tr> <tr>
<th width="40%">Tag</th> <th width="40%">Tag</th>
<th width="20%">Date</th> <th width="20%">Date</th>
<th width="20%">Commit</th> <th width="20%">Commit</th>
<th width="20%">Download</th> <th width="20%">Download</th>
</tr> </tr>
@repository.tags.map { tag => @repository.tags.map { tag =>
<tr> <tr>
<td><a href="@url(repository)/tree/@encodeRefName(tag.name)">@tag.name</a></td> <td><a href="@url(repository)/tree/@encodeRefName(tag.name)">@tag.name</a></td>
<td>@datetime(tag.time)</td> <td>@datetime(tag.time)</td>
<td class="monospace"><a href="@url(repository)/commit/@tag.id">@tag.id.substring(0, 10)</a></td> <td class="monospace"><a href="@url(repository)/commit/@tag.id">@tag.id.substring(0, 10)</a></td>
<td><a href="@url(repository)/archive/@{encodeRefName(tag.name)}.zip">ZIP</a></td> <td><a href="@url(repository)/archive/@{encodeRefName(tag.name)}.zip">ZIP</a></td>
</tr> </tr>
} }
</table> </table>
} }
} }

View File

@@ -1,38 +1,38 @@
@(active: String, fileCount: Int, issueCount: Int, query: String, @(active: String, fileCount: Int, issueCount: Int, query: String,
repository: service.RepositoryService.RepositoryInfo)(body: Html)(implicit context: app.Context) repository: service.RepositoryService.RepositoryInfo)(body: Html)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.menu("", repository){ @html.menu("", repository){
<div class="row-fluid"> <div class="row-fluid">
<div class="span3"> <div class="span3">
<div class="box"> <div class="box">
<ul class="nav nav-tabs nav-stacked side-menu"> <ul class="nav nav-tabs nav-stacked side-menu">
<li@if(active=="code"){ class="active"}> <li@if(active=="code"){ class="active"}>
<a href="@url(repository)/search?q=@urlEncode(query)&type=code"> <a href="@url(repository)/search?q=@urlEncode(query)&type=code">
@if(fileCount != 0){ @if(fileCount != 0){
<span class="badge pull-right">@fileCount</span> <span class="badge pull-right">@fileCount</span>
} }
Code Code
</a> </a>
</li> </li>
<li@if(active=="issue"){ class="active"}> <li@if(active=="issue"){ class="active"}>
<a href="@url(repository)/search?q=@urlEncode(query)&type=issue"> <a href="@url(repository)/search?q=@urlEncode(query)&type=issue">
@if(issueCount != 0){ @if(issueCount != 0){
<span class="badge pull-right">@issueCount</span> <span class="badge pull-right">@issueCount</span>
} }
Issue Issue
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<div class="span9"> <div class="span9">
<form action="@url(repository)/search" method="GET"> <form action="@url(repository)/search" method="GET">
<input type="text" name="q" value="@query" style="width: 80%; margin-bottom: 0px;"/> <input type="text" name="q" value="@query" style="width: 80%; margin-bottom: 0px;"/>
<input type="submit" value="Search" class="btn" style="width: 15%;"/> <input type="submit" value="Search" class="btn" style="width: 15%;"/>
<input type="hidden" name="type" value="@active"/> <input type="hidden" name="type" value="@active"/>
</form> </form>
@body @body
</div> </div>
</div> </div>
} }

View File

@@ -1,35 +1,35 @@
@(collaborators: List[String], @(collaborators: List[String],
isGroupRepository: Boolean, isGroupRepository: 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._
@html.main("Settings", Some(repository)){ @html.main("Settings", Some(repository)){
@html.menu("settings", repository){ @html.menu("settings", repository){
@menu("collaborators", repository){ @menu("collaborators", repository){
<h3>Manage Collaborators</h3> <h3>Manage Collaborators</h3>
<ul class="collaborator"> <ul class="collaborator">
@collaborators.map { collaboratorName => @collaborators.map { collaboratorName =>
<li> <li>
<a href="@url(collaboratorName)">@collaboratorName</a> <a href="@url(collaboratorName)">@collaboratorName</a>
@if(!isGroupRepository){ @if(!isGroupRepository){
<a href="@url(repository)/settings/collaborators/remove?name=@collaboratorName" class="remove">(remove)</a> <a href="@url(repository)/settings/collaborators/remove?name=@collaboratorName" class="remove">(remove)</a>
} else { } else {
@if(repository.managers.contains(collaboratorName)){ @if(repository.managers.contains(collaboratorName)){
(Manager) (Manager)
} }
} }
</li> </li>
} }
</ul> </ul>
@if(!isGroupRepository){ @if(!isGroupRepository){
<form method="POST" action="@url(repository)/settings/collaborators/add" validate="true" autocomplete="off"> <form method="POST" action="@url(repository)/settings/collaborators/add" validate="true" autocomplete="off">
<div> <div>
<span class="error" id="error-userName"></span> <span class="error" id="error-userName"></span>
</div> </div>
@helper.html.account("userName", 300) @helper.html.account("userName", 300)
<input type="submit" class="btn" value="Add"/> <input type="submit" class="btn" value="Add"/>
</form> </form>
} }
} }
} }
} }

View File

@@ -1,26 +1,26 @@
@(active: String, repository: service.RepositoryService.RepositoryInfo)(body: Html)(implicit context: app.Context) @(active: String, repository: service.RepositoryService.RepositoryInfo)(body: Html)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
<div class="row-fluid"> <div class="row-fluid">
<div class="span3"> <div class="span3">
<div class="box"> <div class="box">
<ul class="nav nav-tabs nav-stacked side-menu"> <ul class="nav nav-tabs nav-stacked side-menu">
<li@if(active=="options"){ class="active"}> <li@if(active=="options"){ class="active"}>
<a href="@url(repository)/settings/options">Options</a> <a href="@url(repository)/settings/options">Options</a>
</li> </li>
<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"}> <li@if(active=="hooks"){ class="active"}>
<a href="@url(repository)/settings/hooks">Service Hooks</a> <a href="@url(repository)/settings/hooks">Service Hooks</a>
</li> </li>
<li@if(active=="danger"){ class="active"}> <li@if(active=="danger"){ class="active"}>
<a href="@url(repository)/settings/danger">Danger Zone</a> <a href="@url(repository)/settings/danger">Danger Zone</a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<div class="span9"> <div class="span9">
@body @body
</div> </div>
</div> </div>

View File

@@ -1,100 +1,100 @@
@(repository: service.RepositoryService.RepositoryInfo, info: Option[Any])(implicit context: app.Context) @(repository: service.RepositoryService.RepositoryInfo, info: Option[Any])(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main("Settings", Some(repository)){ @html.main("Settings", Some(repository)){
@html.menu("settings", repository){ @html.menu("settings", repository){
@menu("options", repository){ @menu("options", repository){
@helper.html.information(info) @helper.html.information(info)
<form id="form" method="post" action="@url(repository)/settings/options" validate="true"> <form id="form" method="post" action="@url(repository)/settings/options" validate="true">
<div class="box"> <div class="box">
<div class="box-header">Settings</div> <div class="box-header">Settings</div>
<div class="box-content"> <div class="box-content">
<fieldset> <fieldset>
<label for="repositoryName" class="strong">Repository Name:</label> <label for="repositoryName" class="strong">Repository Name:</label>
<input type="text" name="repositoryName" id="repositoryName" value="@repository.name"/> <input type="text" name="repositoryName" id="repositoryName" value="@repository.name"/>
<span id="error-repositoryName" class="error"></span> <span id="error-repositoryName" class="error"></span>
</fieldset> </fieldset>
<fieldset class="margin"> <fieldset class="margin">
<label for="description" class="strong">Description:</label> <label for="description" class="strong">Description:</label>
<input type="text" name="description" id="description" style="width: 600px;" value="@repository.repository.description"/> <input type="text" name="description" id="description" style="width: 600px;" value="@repository.repository.description"/>
</fieldset> </fieldset>
<fieldset class="margin"> <fieldset class="margin">
<label for="defaultBranch" class="strong">Default Branch:</label> <label for="defaultBranch" class="strong">Default Branch:</label>
<select name="defaultBranch" id="defaultBranch"@if(repository.branchList.isEmpty){ disabled}> <select name="defaultBranch" id="defaultBranch"@if(repository.branchList.isEmpty){ disabled}>
@if(repository.branchList.isEmpty){ @if(repository.branchList.isEmpty){
<option value="none" selected>No Branch</option> <option value="none" selected>No Branch</option>
} else { } else {
@repository.branchList.map { branch => @repository.branchList.map { branch =>
<option@if(branch==repository.repository.defaultBranch){ selected}>@branch</option> <option@if(branch==repository.repository.defaultBranch){ selected}>@branch</option>
} }
} }
</select> </select>
@if(repository.branchList.isEmpty){ @if(repository.branchList.isEmpty){
<input type="hidden" name="defaultBranch" value="none"/> <input type="hidden" name="defaultBranch" value="none"/>
} }
<span class="error" id="error-defaultBranch"></span> <span class="error" id="error-defaultBranch"></span>
</fieldset> </fieldset>
<fieldset class="margin"> <fieldset class="margin">
<label class="radio"> <label class="radio">
<input type="radio" name="isPrivate" value="false" <input type="radio" name="isPrivate" value="false"
@if(!repository.repository.isPrivate ){ checked } @if(!repository.repository.isPrivate ){ checked }
@if(repository.repository.parentUserName.isDefined){ disabled } @if(repository.repository.parentUserName.isDefined){ disabled }
> >
<span class="strong">Public</span><br> <span class="strong">Public</span><br>
<div> <div>
<span>All users and guests can read this repository.</span> <span>All users and guests can read this repository.</span>
</div> </div>
</label> </label>
</fieldset> </fieldset>
<fieldset> <fieldset>
<label class="radio"> <label class="radio">
<input type="radio" name="isPrivate" value="true" <input type="radio" name="isPrivate" value="true"
@if(repository.repository.isPrivate ){ checked } @if(repository.repository.isPrivate ){ checked }
@if(repository.repository.parentUserName.isDefined){ disabled } @if(repository.repository.parentUserName.isDefined){ disabled }
> >
<span class="strong">Private</span><br> <span class="strong">Private</span><br>
<div> <div>
<span>Only collaborators can read this repository.</span> <span>Only collaborators can read this repository.</span>
</div> </div>
</label> </label>
</fieldset> </fieldset>
</div> </div>
</div> </div>
@* @*
<div class="box"> <div class="box">
<div class="box-header">Features:</div> <div class="box-header">Features:</div>
<div class="box-content"> <div class="box-content">
<dl> <dl>
<dt> <dt>
<label class="checkbox strong"> <label class="checkbox strong">
<input type="checkbox" name="wiki" id="wiki"/> Wiki <input type="checkbox" name="wiki" id="wiki"/> Wiki
</label> </label>
</dt> </dt>
<dd> <dd>
Adds lightweight Wiki system to this repository. Adds lightweight Wiki system to this repository.
This is the simplest way to provide documentation or examples. This is the simplest way to provide documentation or examples.
Only collaborators can edit Wiki pages. Only collaborators can edit Wiki pages.
</dd> </dd>
</dl> </dl>
<hr> <hr>
<dl> <dl>
<dt> <dt>
<label class="checkbox strong"> <label class="checkbox strong">
<input type="checkbox" name="issue" id="issue"/> Issue <input type="checkbox" name="issue" id="issue"/> Issue
</label> </label>
</dt> </dt>
<dd> <dd>
Adds lightweight issue tracking integrated with this repository. Adds lightweight issue tracking integrated with this repository.
All users who have signed in and can access this repository can register an issue. All users who have signed in and can access this repository can register an issue.
</dd> </dd>
</dl> </dl>
</div> </div>
</div> </div>
*@ *@
<fieldset> <fieldset>
<input type="submit" class="btn btn-success" value="Apply changes"/> <input type="submit" class="btn btn-success" value="Apply changes"/>
</fieldset> </fieldset>
</form> </form>
} }
} }
} }

View File

@@ -1,7 +1,7 @@
@()(implicit context: app.Context) @()(implicit context: app.Context)
@import context._ @import context._
@main("Sign in"){ @main("Sign in"){
<div class="signin-form"> <div class="signin-form">
@signinform(settings) @signinform(settings)
</div> </div>
} }

View File

@@ -1,40 +1,40 @@
@(pageName: Option[String], @(pageName: Option[String],
from: String, from: String,
to: String, to: String,
diffs: Seq[util.JGitUtil.DiffInfo], diffs: Seq[util.JGitUtil.DiffInfo],
repository: service.RepositoryService.RepositoryInfo, repository: service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean, hasWritePermission: Boolean,
info: Option[Any])(implicit context: app.Context) info: Option[Any])(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
@html.main(s"Compare Revisions - ${repository.owner}/${repository.name}", Some(repository)){ @html.main(s"Compare Revisions - ${repository.owner}/${repository.name}", Some(repository)){
@helper.html.information(info) @helper.html.information(info)
@html.menu("wiki", repository){ @html.menu("wiki", repository){
<ul class="nav nav-tabs fill-width pull-left"> <ul class="nav nav-tabs fill-width pull-left">
<li> <li>
<h1 class="wiki-title"><span class="muted">Compare Revisions</span></h1> <h1 class="wiki-title"><span class="muted">Compare Revisions</span></h1>
</li> </li>
<li class="pull-right"> <li class="pull-right">
<div class="btn-group"> <div class="btn-group">
@if(pageName.isDefined){ @if(pageName.isDefined){
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)">View Page</a> <a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)">View Page</a>
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_history">Back to Page History</a> <a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_history">Back to Page History</a>
} else { } else {
<a class="btn btn-small" href="@url(repository)/wiki/_history">Back to Wiki History</a> <a class="btn btn-small" href="@url(repository)/wiki/_history">Back to Wiki History</a>
} }
</div> </div>
</li> </li>
</ul> </ul>
@helper.html.diff(diffs, repository, None, None, false) @helper.html.diff(diffs, repository, None, None, false)
@if(hasWritePermission){ @if(hasWritePermission){
<div> <div>
@if(pageName.isDefined){ @if(pageName.isDefined){
<a href="@url(repository)/wiki/@urlEncode(pageName)/_revert/@from...@to" class="btn">Revert Changes</a> <a href="@url(repository)/wiki/@urlEncode(pageName)/_revert/@from...@to" class="btn">Revert Changes</a>
} else { } else {
<a href="@url(repository)/wiki/_revert/@from...@to" class="btn">Revert Changes</a> <a href="@url(repository)/wiki/_revert/@from...@to" class="btn">Revert Changes</a>
} }
</div> </div>
} }
} }
} }

View File

@@ -43,7 +43,7 @@
<div class="small"> <div class="small">
<strong>Clone this wiki locally</strong> <strong>Clone this wiki locally</strong>
</div> </div>
@helper.html.copy("repository-url-copy", repository.httpUrl){ @helper.html.copy("repository-url-copy", httpUrl(repository)){
<input type="text" value="@httpUrl(repository)" id="repository-url" style="width: 160px;" readonly> <input type="text" value="@httpUrl(repository)" id="repository-url" style="width: 160px;" readonly>
} }
@if(settings.ssh && loginAccount.isDefined){ @if(settings.ssh && loginAccount.isDefined){

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3,7 +3,6 @@
/****************************************************************************/ /****************************************************************************/
body { body {
color: #333; color: #333;
line-height: 100%;
} }
li p { li p {
@@ -98,6 +97,13 @@ div.input-prepend span.count {
padding-bottom: 6px; padding-bottom: 6px;
} }
img.plugin-global-menu {
width: 16px;
height: 16px;
position: relative;
top: -2px;
}
/* ======================================================================== */ /* ======================================================================== */
/* General Styles */ /* General Styles */
/* ======================================================================== */ /* ======================================================================== */
@@ -859,12 +865,58 @@ ul.collaborator a.remove {
/****************************************************************************/ /****************************************************************************/
/* Markdown */ /* Markdown */
/****************************************************************************/ /****************************************************************************/
div.markdown-body {
line-height: 1.7;
font: 15px Helvetica, arial, freesans, clean, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol";
word-wrap: break-word;
}
div.markdown-body h1 { div.markdown-body h1 {
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
font-size: 2.5em;
font-weight: bold;
} }
div.markdown-body h2 { div.markdown-body h2 {
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
font-size: 2em;
margin-top: 30px;
}
div.markdown-body h3 {
font-size: 1.5em;
}
div.markdown-body h4 {
font-size: 1.2em;
}
div.markdown-body h5 {
font-size: 1em;
}
div.markdown-body h6 {
color:#777;
font-size: 1em;
}
div.markdown-body li {
line-height: 1.7;
}
div.markdown-body p {
margin: 15px 0;
line-height: 1.7;
}
div.markdown-body pre {
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 12px;
white-space: pre;
}
div.markdown-body code {
font-size: 12px;
padding: 0 5px;
} }
div.markdown-body table { div.markdown-body table {
@@ -1002,6 +1054,7 @@ div.markdown-body table colgroup + tbody tr:first-child td:last-child {
.markdown-head { .markdown-head {
position: relative; position: relative;
line-height: 1.7;
} }
a.markdown-anchor-link { a.markdown-anchor-link {
@@ -1015,25 +1068,25 @@ a.markdown-anchor-link {
} }
h1 a.markdown-anchor-link { h1 a.markdown-anchor-link {
top: 8px; top: 24px;
} }
h2 a.markdown-anchor-link { h2 a.markdown-anchor-link {
top: 6px; top: 20px;
} }
h3 a.markdown-anchor-link { h3 a.markdown-anchor-link {
top: 6px; top: 12px;
} }
h4 a.markdown-anchor-link { h4 a.markdown-anchor-link {
top: 2px; top: 8px;
} }
h5 a.markdown-anchor-link { h5 a.markdown-anchor-link {
top: 2px; top: 6px;
} }
h6 a.markdown-anchor-link { h6 a.markdown-anchor-link {
top: 2px; top: 6px;
} }

View File

@@ -1,105 +1,109 @@
$(function(){ $(function(){
// disable Ajax cache // disable Ajax cache
$.ajaxSetup({ cache: false }); $.ajaxSetup({ cache: false });
// repository url text field // repository url text field
$('#repository-url').click(function(){ $('#repository-url').click(function(){
this.select(0, this.value.length); this.select(0, this.value.length);
}); });
// activate tooltip // activate tooltip
$('img[data-toggle=tooltip]').tooltip(); $('img[data-toggle=tooltip]').tooltip();
$('a[data-toggle=tooltip]').tooltip(); $('a[data-toggle=tooltip]').tooltip();
// anchor icon for markdown // anchor icon for markdown
$('.markdown-head').mouseenter(function(e){ $('.markdown-head').mouseenter(function(e){
$(e.target).children('a.markdown-anchor-link').show(); $(e.target).children('a.markdown-anchor-link').show();
}); });
$('.markdown-head').mouseleave(function(e){ $('.markdown-head').mouseleave(function(e){
var anchorLink = $(e.target).children('a.markdown-anchor-link'); var anchorLink = $(e.target).children('a.markdown-anchor-link');
if(anchorLink.data('active') != true){ if(anchorLink.data('active') != true){
anchorLink.hide(); anchorLink.hide();
} }
}); });
$('a.markdown-anchor-link').mouseenter(function(e){ $('a.markdown-anchor-link').mouseenter(function(e){
$(e.target).data('active', true); $(e.target).data('active', true);
}); });
$('a.markdown-anchor-link').mouseleave(function(e){ $('a.markdown-anchor-link').mouseleave(function(e){
$(e.target).data('active', false); $(e.target).data('active', false);
$(e.target).hide(); $(e.target).hide();
}); });
// syntax highlighting by google-code-prettify // syntax highlighting by google-code-prettify
prettyPrint(); prettyPrint();
}); });
function displayErrors(data){ function displayErrors(data){
var i = 0; var i = 0;
$.each(data, function(key, value){ $.each(data, function(key, value){
$('#error-' + key.split(".").join("_")).text(value); $('#error-' + key.split(".").join("_")).text(value);
if(i == 0){ if(i == 0){
$('#' + key).focus(); $('#' + key).focus();
} }
i++; i++;
}); });
} }
(function($){ (function($){
$.fn.watch = function(callback){ $.fn.watch = function(callback){
var timer = null; var timer = null;
var prevValue = this.val(); var prevValue = this.val();
this.on('focus', function(e){ this.on('focus', function(e){
window.clearInterval(timer); window.clearInterval(timer);
timer = window.setInterval(function(){ timer = window.setInterval(function(){
var newValue = $(e.target).val(); var newValue = $(e.target).val();
if(prevValue != newValue){ if(prevValue != newValue){
callback(); callback();
} }
prevValue = newValue; prevValue = newValue;
}, 10); }, 10);
}); });
this.on('blur', function(){ this.on('blur', function(){
window.clearInterval(timer); window.clearInterval(timer);
}); });
}; };
})(jQuery); })(jQuery);
function diffUsingJS(oldTextId, newTextId, outputId) { function diffUsingJS(oldTextId, newTextId, outputId) {
// get the baseText and newText values from the two textboxes, and split them into lines // get the baseText and newText values from the two textboxes, and split them into lines
var oldText = document.getElementById(oldTextId).value; var oldText = document.getElementById(oldTextId).value;
if(oldText == ''){ if(oldText == ''){
var oldLines = []; var oldLines = [];
} else { } else {
var oldLines = difflib.stringAsLines(oldText); var oldLines = difflib.stringAsLines(oldText);
} }
var newText = document.getElementById(newTextId).value var newText = document.getElementById(newTextId).value
if(newText == ''){ if(newText == ''){
var newLines = []; var newLines = [];
} else { } else {
var newLines = difflib.stringAsLines(newText); var newLines = difflib.stringAsLines(newText);
} }
// create a SequenceMatcher instance that diffs the two sets of lines // create a SequenceMatcher instance that diffs the two sets of lines
var sm = new difflib.SequenceMatcher(oldLines, newLines); var sm = new difflib.SequenceMatcher(oldLines, newLines);
// get the opcodes from the SequenceMatcher instance // get the opcodes from the SequenceMatcher instance
// opcodes is a list of 3-tuples describing what changes should be made to the base text // opcodes is a list of 3-tuples describing what changes should be made to the base text
// in order to yield the new text // in order to yield the new text
var opcodes = sm.get_opcodes(); var opcodes = sm.get_opcodes();
var diffoutputdiv = document.getElementById(outputId); var diffoutputdiv = document.getElementById(outputId);
while (diffoutputdiv.firstChild) diffoutputdiv.removeChild(diffoutputdiv.firstChild); while (diffoutputdiv.firstChild) diffoutputdiv.removeChild(diffoutputdiv.firstChild);
// build the diff view and add it to the current DOM // build the diff view and add it to the current DOM
diffoutputdiv.appendChild(diffview.buildView({ diffoutputdiv.appendChild(diffview.buildView({
baseTextLines: oldLines, baseTextLines: oldLines,
newTextLines: newLines, newTextLines: newLines,
opcodes: opcodes, opcodes: opcodes,
contextSize: 4, contextSize: 4,
viewType: 1 viewType: 1
})); }));
}
function jqSelectorEscape(val) {
return val.replace(/[!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~]/g, '\\$&');
} }

View File

@@ -1,40 +1,40 @@
$(function(){ $(function(){
$.each($('form[validate=true]'), function(i, form){ $.each($('form[validate=true]'), function(i, form){
$(form).submit(validate); $(form).submit(validate);
}); });
$.each($('input[formaction]'), function(i, input){ $.each($('input[formaction]'), function(i, input){
$(input).click(function(){ $(input).click(function(){
var form = $(input).parents('form') var form = $(input).parents('form')
$(form).attr('action', $(input).attr('formaction')) $(form).attr('action', $(input).attr('formaction'))
}); });
}); });
}); });
function validate(e){ function validate(e){
var form = $(e.target); var form = $(e.target);
if(form.data('validated') == true){ if(form.data('validated') == true){
return true; return true;
} }
$.post(form.attr('action') + '/validate', $(e.target).serialize(), function(data){ $.post(form.attr('action') + '/validate', $(e.target).serialize(), function(data){
// clear all error messages // clear all error messages
$('.error').text(''); $('.error').text('');
if($.isEmptyObject(data)){ if($.isEmptyObject(data)){
form.data('validated', true); form.data('validated', true);
form.submit(); form.submit();
form.data('validated', false); form.data('validated', false);
} else { } else {
form.data('validated', false); form.data('validated', false);
displayErrors(data); displayErrors(data);
} }
}, 'json'); }, 'json');
return false; return false;
} }
function displayErrors(data){ function displayErrors(data){
$.each(data, function(key, value){ $.each(data, function(key, value){
$('#error-' + key.split(".").join("_")).text(value); $('#error-' + key.split(".").join("_")).text(value);
}); });
} }

Some files were not shown because too many files have changed in this diff Show More