diff --git a/src/main/resources/update/1_0.sql b/src/main/resources/update/1_0.sql index 418c3e3..5067ada 100644 --- a/src/main/resources/update/1_0.sql +++ b/src/main/resources/update/1_0.sql @@ -1,135 +1,135 @@ -CREATE TABLE ACCOUNT( - USER_NAME VARCHAR(100) NOT NULL, - MAIL_ADDRESS VARCHAR(100) NOT NULL, - PASSWORD VARCHAR(40) NOT NULL, - ADMINISTRATOR BOOLEAN NOT NULL, - URL VARCHAR(200), - REGISTERED_DATE TIMESTAMP NOT NULL, - UPDATED_DATE TIMESTAMP NOT NULL, - LAST_LOGIN_DATE TIMESTAMP -); - -CREATE TABLE REPOSITORY( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - PRIVATE BOOLEAN NOT NULL, - DESCRIPTION TEXT, - DEFAULT_BRANCH VARCHAR(100), - REGISTERED_DATE TIMESTAMP NOT NULL, - UPDATED_DATE TIMESTAMP NOT NULL, - LAST_ACTIVITY_DATE TIMESTAMP NOT NULL -); - -CREATE TABLE COLLABORATOR( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - COLLABORATOR_NAME VARCHAR(100) NOT NULL -); - -CREATE TABLE ISSUE( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - ISSUE_ID INT NOT NULL, - OPENED_USER_NAME VARCHAR(100) NOT NULL, - MILESTONE_ID INT, - ASSIGNED_USER_NAME VARCHAR(100), - TITLE TEXT NOT NULL, - CONTENT TEXT, - CLOSED BOOLEAN NOT NULL, - REGISTERED_DATE TIMESTAMP NOT NULL, - UPDATED_DATE TIMESTAMP NOT NULL -); - -CREATE TABLE ISSUE_ID( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - ISSUE_ID INT NOT NULL -); - -CREATE TABLE ISSUE_COMMENT( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - ISSUE_ID INT NOT NULL, - COMMENT_ID INT AUTO_INCREMENT, - ACTION VARCHAR(10), - COMMENTED_USER_NAME VARCHAR(100) NOT NULL, - CONTENT TEXT NOT NULL, - REGISTERED_DATE TIMESTAMP NOT NULL, - UPDATED_DATE TIMESTAMP NOT NULL -); - -CREATE TABLE LABEL( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - LABEL_ID INT AUTO_INCREMENT, - LABEL_NAME VARCHAR(100) NOT NULL, - COLOR CHAR(6) NOT NULL -); - -CREATE TABLE ISSUE_LABEL( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - ISSUE_ID INT NOT NULL, - LABEL_ID INT NOT NULL -); - -CREATE TABLE MILESTONE( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - MILESTONE_ID INT AUTO_INCREMENT, - TITLE VARCHAR(100) NOT NULL, - DESCRIPTION TEXT, - DUE_DATE TIMESTAMP, - CLOSED_DATE TIMESTAMP -); - -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 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 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_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_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_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_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_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 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 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 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); - -INSERT INTO ACCOUNT ( - USER_NAME, - MAIL_ADDRESS, - PASSWORD, - ADMINISTRATOR, - URL, - REGISTERED_DATE, - UPDATED_DATE, - LAST_LOGIN_DATE -) VALUES ( - 'root', - 'root@localhost', - 'dc76e9f0c0006e8f919e0c515c66dbba3982f785', - true, - 'https://github.com/takezoe/gitbucket', - SYSDATE, - SYSDATE, - NULL -); +CREATE TABLE ACCOUNT( + USER_NAME VARCHAR(100) NOT NULL, + MAIL_ADDRESS VARCHAR(100) NOT NULL, + PASSWORD VARCHAR(40) NOT NULL, + ADMINISTRATOR BOOLEAN NOT NULL, + URL VARCHAR(200), + REGISTERED_DATE TIMESTAMP NOT NULL, + UPDATED_DATE TIMESTAMP NOT NULL, + LAST_LOGIN_DATE TIMESTAMP +); + +CREATE TABLE REPOSITORY( + USER_NAME VARCHAR(100) NOT NULL, + REPOSITORY_NAME VARCHAR(100) NOT NULL, + PRIVATE BOOLEAN NOT NULL, + DESCRIPTION TEXT, + DEFAULT_BRANCH VARCHAR(100), + REGISTERED_DATE TIMESTAMP NOT NULL, + UPDATED_DATE TIMESTAMP NOT NULL, + LAST_ACTIVITY_DATE TIMESTAMP NOT NULL +); + +CREATE TABLE COLLABORATOR( + USER_NAME VARCHAR(100) NOT NULL, + REPOSITORY_NAME VARCHAR(100) NOT NULL, + COLLABORATOR_NAME VARCHAR(100) NOT NULL +); + +CREATE TABLE ISSUE( + USER_NAME VARCHAR(100) NOT NULL, + REPOSITORY_NAME VARCHAR(100) NOT NULL, + ISSUE_ID INT NOT NULL, + OPENED_USER_NAME VARCHAR(100) NOT NULL, + MILESTONE_ID INT, + ASSIGNED_USER_NAME VARCHAR(100), + TITLE TEXT NOT NULL, + CONTENT TEXT, + CLOSED BOOLEAN NOT NULL, + REGISTERED_DATE TIMESTAMP NOT NULL, + UPDATED_DATE TIMESTAMP NOT NULL +); + +CREATE TABLE ISSUE_ID( + USER_NAME VARCHAR(100) NOT NULL, + REPOSITORY_NAME VARCHAR(100) NOT NULL, + ISSUE_ID INT NOT NULL +); + +CREATE TABLE ISSUE_COMMENT( + USER_NAME VARCHAR(100) NOT NULL, + REPOSITORY_NAME VARCHAR(100) NOT NULL, + ISSUE_ID INT NOT NULL, + COMMENT_ID INT AUTO_INCREMENT, + ACTION VARCHAR(10), + COMMENTED_USER_NAME VARCHAR(100) NOT NULL, + CONTENT TEXT NOT NULL, + REGISTERED_DATE TIMESTAMP NOT NULL, + UPDATED_DATE TIMESTAMP NOT NULL +); + +CREATE TABLE LABEL( + USER_NAME VARCHAR(100) NOT NULL, + REPOSITORY_NAME VARCHAR(100) NOT NULL, + LABEL_ID INT AUTO_INCREMENT, + LABEL_NAME VARCHAR(100) NOT NULL, + COLOR CHAR(6) NOT NULL +); + +CREATE TABLE ISSUE_LABEL( + USER_NAME VARCHAR(100) NOT NULL, + REPOSITORY_NAME VARCHAR(100) NOT NULL, + ISSUE_ID INT NOT NULL, + LABEL_ID INT NOT NULL +); + +CREATE TABLE MILESTONE( + USER_NAME VARCHAR(100) NOT NULL, + REPOSITORY_NAME VARCHAR(100) NOT NULL, + MILESTONE_ID INT AUTO_INCREMENT, + TITLE VARCHAR(100) NOT NULL, + DESCRIPTION TEXT, + DUE_DATE TIMESTAMP, + CLOSED_DATE TIMESTAMP +); + +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 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 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_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_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_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_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_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 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 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 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); + +INSERT INTO ACCOUNT ( + USER_NAME, + MAIL_ADDRESS, + PASSWORD, + ADMINISTRATOR, + URL, + REGISTERED_DATE, + UPDATED_DATE, + LAST_LOGIN_DATE +) VALUES ( + 'root', + 'root@localhost', + 'dc76e9f0c0006e8f919e0c515c66dbba3982f785', + true, + 'https://github.com/takezoe/gitbucket', + SYSDATE, + SYSDATE, + NULL +); diff --git a/src/main/scala/app/DashboardController.scala b/src/main/scala/app/DashboardController.scala index bf5e11e..bd788c9 100644 --- a/src/main/scala/app/DashboardController.scala +++ b/src/main/scala/app/DashboardController.scala @@ -1,109 +1,109 @@ -package app - -import service._ -import util.{UsersAuthenticator, Keys} -import util.Implicits._ - -class DashboardController extends DashboardControllerBase - with IssuesService with PullRequestService with RepositoryService with AccountService - with UsersAuthenticator - -trait DashboardControllerBase extends ControllerBase { - self: IssuesService with PullRequestService with RepositoryService with UsersAuthenticator => - - get("/dashboard/issues/repos")(usersOnly { - searchIssues("all") - }) - - get("/dashboard/issues/assigned")(usersOnly { - searchIssues("assigned") - }) - - get("/dashboard/issues/created_by")(usersOnly { - searchIssues("created_by") - }) - - get("/dashboard/pulls")(usersOnly { - searchPullRequests("created_by", None) - }) - - get("/dashboard/pulls/owned")(usersOnly { - searchPullRequests("created_by", None) - }) - - get("/dashboard/pulls/public")(usersOnly { - searchPullRequests("not_created_by", None) - }) - - get("/dashboard/pulls/for/:owner/:repository")(usersOnly { - searchPullRequests("all", Some(params("owner") + "/" + params("repository"))) - }) - - private def searchIssues(filter: String) = { - import IssuesService._ - - // condition - val condition = session.putAndGet(Keys.Session.DashboardIssues, - if(request.hasQueryString) IssueSearchCondition(request) - else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition()) - ) - - val userName = context.loginAccount.get.userName - val repositories = getUserRepositories(userName, context.baseUrl).map(repo => repo.owner -> repo.name) - val filterUser = Map(filter -> userName) - val page = IssueSearchCondition.page(request) - // - dashboard.html.issues( - issues.html.listparts( - searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, repositories: _*), - page, - countIssue(condition.copy(state = "open"), filterUser, false, repositories: _*), - countIssue(condition.copy(state = "closed"), filterUser, false, repositories: _*), - condition), - countIssue(condition, Map.empty, false, repositories: _*), - countIssue(condition, Map("assigned" -> userName), false, repositories: _*), - countIssue(condition, Map("created_by" -> userName), false, repositories: _*), - countIssueGroupByRepository(condition, filterUser, false, repositories: _*), - condition, - filter) - - } - - private def searchPullRequests(filter: String, repository: Option[String]) = { - import IssuesService._ - import PullRequestService._ - - // condition - val condition = session.putAndGet(Keys.Session.DashboardPulls, { - if(request.hasQueryString) IssueSearchCondition(request) - else session.getAs[IssueSearchCondition](Keys.Session.DashboardPulls).getOrElse(IssueSearchCondition()) - }.copy(repo = repository)) - - val userName = context.loginAccount.get.userName - val repositories = getUserRepositories(userName, context.baseUrl).map(repo => repo.owner -> repo.name) - val filterUser = Map(filter -> userName) - val page = IssueSearchCondition.page(request) - - val counts = countIssueGroupByRepository( - IssueSearchCondition().copy(state = condition.state), Map.empty, true, repositories: _*) - - dashboard.html.pulls( - pulls.html.listparts( - searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, repositories: _*), - page, - countIssue(condition.copy(state = "open"), filterUser, true, repositories: _*), - countIssue(condition.copy(state = "closed"), filterUser, true, repositories: _*), - condition, - None, - false), - getPullRequestCountGroupByUser(condition.state == "closed", None, None), - getRepositoryNamesOfUser(userName).map { RepoName => - (userName, RepoName, counts.collectFirst { case (_, RepoName, count) => count }.getOrElse(0)) - }.sortBy(_._3).reverse, - condition, - filter) - - } - - -} +package app + +import service._ +import util.{UsersAuthenticator, Keys} +import util.Implicits._ + +class DashboardController extends DashboardControllerBase + with IssuesService with PullRequestService with RepositoryService with AccountService + with UsersAuthenticator + +trait DashboardControllerBase extends ControllerBase { + self: IssuesService with PullRequestService with RepositoryService with UsersAuthenticator => + + get("/dashboard/issues/repos")(usersOnly { + searchIssues("all") + }) + + get("/dashboard/issues/assigned")(usersOnly { + searchIssues("assigned") + }) + + get("/dashboard/issues/created_by")(usersOnly { + searchIssues("created_by") + }) + + get("/dashboard/pulls")(usersOnly { + searchPullRequests("created_by", None) + }) + + get("/dashboard/pulls/owned")(usersOnly { + searchPullRequests("created_by", None) + }) + + get("/dashboard/pulls/public")(usersOnly { + searchPullRequests("not_created_by", None) + }) + + get("/dashboard/pulls/for/:owner/:repository")(usersOnly { + searchPullRequests("all", Some(params("owner") + "/" + params("repository"))) + }) + + private def searchIssues(filter: String) = { + import IssuesService._ + + // condition + val condition = session.putAndGet(Keys.Session.DashboardIssues, + if(request.hasQueryString) IssueSearchCondition(request) + else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition()) + ) + + val userName = context.loginAccount.get.userName + val repositories = getUserRepositories(userName, context.baseUrl).map(repo => repo.owner -> repo.name) + val filterUser = Map(filter -> userName) + val page = IssueSearchCondition.page(request) + // + dashboard.html.issues( + issues.html.listparts( + searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, repositories: _*), + page, + countIssue(condition.copy(state = "open"), filterUser, false, repositories: _*), + countIssue(condition.copy(state = "closed"), filterUser, false, repositories: _*), + condition), + countIssue(condition, Map.empty, false, repositories: _*), + countIssue(condition, Map("assigned" -> userName), false, repositories: _*), + countIssue(condition, Map("created_by" -> userName), false, repositories: _*), + countIssueGroupByRepository(condition, filterUser, false, repositories: _*), + condition, + filter) + + } + + private def searchPullRequests(filter: String, repository: Option[String]) = { + import IssuesService._ + import PullRequestService._ + + // condition + val condition = session.putAndGet(Keys.Session.DashboardPulls, { + if(request.hasQueryString) IssueSearchCondition(request) + else session.getAs[IssueSearchCondition](Keys.Session.DashboardPulls).getOrElse(IssueSearchCondition()) + }.copy(repo = repository)) + + val userName = context.loginAccount.get.userName + val repositories = getUserRepositories(userName, context.baseUrl).map(repo => repo.owner -> repo.name) + val filterUser = Map(filter -> userName) + val page = IssueSearchCondition.page(request) + + val counts = countIssueGroupByRepository( + IssueSearchCondition().copy(state = condition.state), Map.empty, true, repositories: _*) + + dashboard.html.pulls( + pulls.html.listparts( + searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, repositories: _*), + page, + countIssue(condition.copy(state = "open"), filterUser, true, repositories: _*), + countIssue(condition.copy(state = "closed"), filterUser, true, repositories: _*), + condition, + None, + false), + getPullRequestCountGroupByUser(condition.state == "closed", None, None), + getRepositoryNamesOfUser(userName).map { RepoName => + (userName, RepoName, counts.collectFirst { case (_, RepoName, count) => count }.getOrElse(0)) + }.sortBy(_._3).reverse, + condition, + filter) + + } + + +} diff --git a/src/main/scala/app/IndexController.scala b/src/main/scala/app/IndexController.scala index 8866795..f5eb2d8 100644 --- a/src/main/scala/app/IndexController.scala +++ b/src/main/scala/app/IndexController.scala @@ -1,85 +1,85 @@ -package app - -import util._ -import service._ -import jp.sf.amateras.scalatra.forms._ - -class IndexController extends IndexControllerBase - with RepositoryService with ActivityService with AccountService with UsersAuthenticator - -trait IndexControllerBase extends ControllerBase { - self: RepositoryService with ActivityService with AccountService with UsersAuthenticator => - - case class SignInForm(userName: String, password: String) - - val form = mapping( - "userName" -> trim(label("Username", text(required))), - "password" -> trim(label("Password", text(required))) - )(SignInForm.apply) - - get("/"){ - val loginAccount = context.loginAccount - - html.index(getRecentActivities(), - getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true), - loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil) - ) - } - - get("/signin"){ - val redirect = params.get("redirect") - if(redirect.isDefined && redirect.get.startsWith("/")){ - flash += Keys.Flash.Redirect -> redirect.get - } - html.signin() - } - - post("/signin", form){ form => - authenticate(context.settings, form.userName, form.password) match { - case Some(account) => signin(account) - case None => redirect("/signin") - } - } - - get("/signout"){ - session.invalidate - redirect("/") - } - - get("/activities.atom"){ - contentType = "application/atom+xml; type=feed" - helper.xml.feed(getRecentActivities()) - } - - /** - * Set account information into HttpSession and redirect. - */ - private def signin(account: model.Account) = { - session.setAttribute(Keys.Session.LoginAccount, account) - updateLastLoginDate(account.userName) - - flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl => - if(redirectUrl.stripSuffix("/") == request.getContextPath){ - redirect("/") - } else { - redirect(redirectUrl) - } - }.getOrElse { - redirect("/") - } - } - - /** - * JSON API for collaborator completion. - * - * TODO Move to other controller? - */ - get("/_user/proposals")(usersOnly { - contentType = formats("json") - org.json4s.jackson.Serialization.write( - Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray) - ) - }) - - -} +package app + +import util._ +import service._ +import jp.sf.amateras.scalatra.forms._ + +class IndexController extends IndexControllerBase + with RepositoryService with ActivityService with AccountService with UsersAuthenticator + +trait IndexControllerBase extends ControllerBase { + self: RepositoryService with ActivityService with AccountService with UsersAuthenticator => + + case class SignInForm(userName: String, password: String) + + val form = mapping( + "userName" -> trim(label("Username", text(required))), + "password" -> trim(label("Password", text(required))) + )(SignInForm.apply) + + get("/"){ + val loginAccount = context.loginAccount + + html.index(getRecentActivities(), + getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true), + loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil) + ) + } + + get("/signin"){ + val redirect = params.get("redirect") + if(redirect.isDefined && redirect.get.startsWith("/")){ + flash += Keys.Flash.Redirect -> redirect.get + } + html.signin() + } + + post("/signin", form){ form => + authenticate(context.settings, form.userName, form.password) match { + case Some(account) => signin(account) + case None => redirect("/signin") + } + } + + get("/signout"){ + session.invalidate + redirect("/") + } + + get("/activities.atom"){ + contentType = "application/atom+xml; type=feed" + helper.xml.feed(getRecentActivities()) + } + + /** + * Set account information into HttpSession and redirect. + */ + private def signin(account: model.Account) = { + session.setAttribute(Keys.Session.LoginAccount, account) + updateLastLoginDate(account.userName) + + flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl => + if(redirectUrl.stripSuffix("/") == request.getContextPath){ + redirect("/") + } else { + redirect(redirectUrl) + } + }.getOrElse { + redirect("/") + } + } + + /** + * JSON API for collaborator completion. + * + * TODO Move to other controller? + */ + get("/_user/proposals")(usersOnly { + contentType = formats("json") + org.json4s.jackson.Serialization.write( + Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray) + ) + }) + + +} diff --git a/src/main/scala/app/IssuesController.scala b/src/main/scala/app/IssuesController.scala index 661c1e0..4f28466 100644 --- a/src/main/scala/app/IssuesController.scala +++ b/src/main/scala/app/IssuesController.scala @@ -1,403 +1,403 @@ -package app - -import jp.sf.amateras.scalatra.forms._ - -import service._ -import IssuesService._ -import util._ -import util.Implicits._ -import util.ControlUtil._ -import org.scalatra.Ok -import model.Issue - -class IssuesController extends IssuesControllerBase - with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService - with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator - -trait IssuesControllerBase extends ControllerBase { - self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService - with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator => - - case class IssueCreateForm(title: String, content: Option[String], - assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String]) - case class IssueEditForm(title: String, content: Option[String]) - case class CommentForm(issueId: Int, content: String) - case class IssueStateForm(issueId: Int, content: Option[String]) - - val issueCreateForm = mapping( - "title" -> trim(label("Title", text(required))), - "content" -> trim(optional(text())), - "assignedUserName" -> trim(optional(text())), - "milestoneId" -> trim(optional(number())), - "labelNames" -> trim(optional(text())) - )(IssueCreateForm.apply) - - val issueEditForm = mapping( - "title" -> trim(label("Title", text(required))), - "content" -> trim(optional(text())) - )(IssueEditForm.apply) - - val commentForm = mapping( - "issueId" -> label("Issue Id", number()), - "content" -> trim(label("Comment", text(required))) - )(CommentForm.apply) - - val issueStateForm = mapping( - "issueId" -> label("Issue Id", number()), - "content" -> trim(optional(text())) - )(IssueStateForm.apply) - - get("/:owner/:repository/issues")(referrersOnly { - searchIssues("all", _) - }) - - get("/:owner/:repository/issues/assigned/:userName")(referrersOnly { - searchIssues("assigned", _) - }) - - get("/:owner/:repository/issues/created_by/:userName")(referrersOnly { - searchIssues("created_by", _) - }) - - get("/:owner/:repository/issues/:id")(referrersOnly { repository => - defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) => - getIssue(owner, name, issueId) map { - issues.html.issue( - _, - getComments(owner, name, issueId.toInt), - getIssueLabels(owner, name, issueId.toInt), - (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, - getMilestonesWithIssueCount(owner, name), - getLabels(owner, name), - hasWritePermission(owner, name, context.loginAccount), - repository) - } getOrElse NotFound - } - }) - - get("/:owner/:repository/issues/new")(readableUsersOnly { repository => - defining(repository.owner, repository.name){ case (owner, name) => - issues.html.create( - (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, - getMilestones(owner, name), - getLabels(owner, name), - hasWritePermission(owner, name, context.loginAccount), - repository) - } - }) - - post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) => - defining(repository.owner, repository.name){ case (owner, name) => - val writable = hasWritePermission(owner, name, context.loginAccount) - val userName = context.loginAccount.get.userName - - // insert issue - val issueId = createIssue(owner, name, userName, form.title, form.content, - if(writable) form.assignedUserName else None, - if(writable) form.milestoneId else None) - - // insert labels - if(writable){ - form.labelNames.map { value => - val labels = getLabels(owner, name) - value.split(",").foreach { labelName => - labels.find(_.labelName == labelName).map { label => - registerIssueLabel(owner, name, issueId, label.labelId) - } - } - } - } - - // record activity - recordCreateIssueActivity(owner, name, userName, issueId, form.title) - - // extract references and create refer comment - getIssue(owner, name, issueId.toString).foreach { issue => - createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) - } - - // notifications - Notifier().toNotify(repository, issueId, form.content.getOrElse("")){ - Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") - } - - redirect(s"/${owner}/${name}/issues/${issueId}") - } - }) - - ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) => - defining(repository.owner, repository.name){ case (owner, name) => - getIssue(owner, name, params("id")).map { issue => - if(isEditable(owner, name, issue.openedUserName)){ - // update issue - updateIssue(owner, name, issue.issueId, form.title, form.content) - // extract references and create refer comment - createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) - - redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") - } else Unauthorized - } getOrElse NotFound - } - }) - - post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => - handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) => - redirect(s"/${repository.owner}/${repository.name}/${ - if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") - } getOrElse NotFound - }) - - post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) => - handleComment(form.issueId, form.content, repository)() map { case (issue, id) => - redirect(s"/${repository.owner}/${repository.name}/${ - if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") - } getOrElse NotFound - }) - - ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => - defining(repository.owner, repository.name){ case (owner, name) => - getComment(owner, name, params("id")).map { comment => - if(isEditable(owner, name, comment.commentedUserName)){ - updateComment(comment.commentId, form.content) - redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}") - } else Unauthorized - } getOrElse NotFound - } - }) - - ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository => - defining(repository.owner, repository.name){ case (owner, name) => - getComment(owner, name, params("id")).map { comment => - if(isEditable(owner, name, comment.commentedUserName)){ - Ok(deleteComment(comment.commentId)) - } else Unauthorized - } getOrElse NotFound - } - }) - - ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository => - getIssue(repository.owner, repository.name, params("id")) map { x => - if(isEditable(x.userName, x.repositoryName, x.openedUserName)){ - params.get("dataType") collect { - case t if t == "html" => issues.html.editissue( - x.title, x.content, x.issueId, x.userName, x.repositoryName) - } getOrElse { - contentType = formats("json") - org.json4s.jackson.Serialization.write( - Map("title" -> x.title, - "content" -> view.Markdown.toHtml(x.content getOrElse "No description given.", - repository, false, true) - )) - } - } else Unauthorized - } getOrElse NotFound - }) - - ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository => - getComment(repository.owner, repository.name, params("id")) map { x => - if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){ - params.get("dataType") collect { - case t if t == "html" => issues.html.editcomment( - x.content, x.commentId, x.userName, x.repositoryName) - } getOrElse { - contentType = formats("json") - org.json4s.jackson.Serialization.write( - Map("content" -> view.Markdown.toHtml(x.content, - repository, false, true) - )) - } - } else Unauthorized - } getOrElse NotFound - }) - - ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository => - defining(params("id").toInt){ issueId => - registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) - issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) - } - }) - - ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository => - defining(params("id").toInt){ issueId => - deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) - issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) - } - }) - - ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository => - updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName")) - Ok("updated") - }) - - ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository => - updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId")) - milestoneId("milestoneId").map { milestoneId => - getMilestonesWithIssueCount(repository.owner, repository.name) - .find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) => - issues.milestones.html.progress(openCount + closeCount, closeCount, false) - } getOrElse NotFound - } getOrElse Ok() - }) - - post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => - defining(params.get("value")){ action => - executeBatch(repository) { - handleComment(_, None, repository)( _ => action) - } - } - }) - - post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository => - params("value").toIntOpt.map{ labelId => - executeBatch(repository) { issueId => - getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse { - registerIssueLabel(repository.owner, repository.name, issueId, labelId) - } - } - } getOrElse NotFound - }) - - post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository => - defining(assignedUserName("value")){ value => - executeBatch(repository) { - updateAssignedUserName(repository.owner, repository.name, _, value) - } - } - }) - - post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository => - defining(milestoneId("value")){ value => - executeBatch(repository) { - updateMilestoneId(repository.owner, repository.name, _, value) - } - } - }) - - get("/:owner/:repository/_attached/:file")(referrersOnly { repository => - (Directory.getAttachedDir(repository.owner, repository.name) match { - case dir if(dir.exists && dir.isDirectory) => - dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file => - contentType = FileUtil.getMimeType(file.getName) - file - } - case _ => None - }) getOrElse NotFound - }) - - val assignedUserName = (key: String) => params.get(key) filter (_.trim != "") - 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 = - hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName - - private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = { - params("checked").split(',') map(_.toInt) foreach execute - redirect(s"/${repository.owner}/${repository.name}/issues") - } - - private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = { - StringUtil.extractIssueId(message).foreach { issueId => - if(getIssue(owner, repository, issueId).isDefined){ - createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, - fromIssue.issueId + ":" + fromIssue.title, "refer") - } - } - } - - /** - * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]] - */ - private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo) - (getAction: model.Issue => Option[String] = - p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = { - - defining(repository.owner, repository.name){ case (owner, name) => - val userName = context.loginAccount.get.userName - - getIssue(owner, name, issueId.toString) map { issue => - val (action, recordActivity) = - getAction(issue) - .collect { - case "close" => true -> (Some("close") -> - Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _)) - case "reopen" => false -> (Some("reopen") -> - Some(recordReopenIssueActivity _)) - } - .map { case (closed, t) => - updateClosed(owner, name, issueId, closed) - t - } - .getOrElse(None -> None) - - val commentId = content - .map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") ) - .getOrElse ( action.get.capitalize -> action.get ) - match { - case (content, action) => createComment(owner, name, userName, issueId, content, action) - } - - // record activity - content foreach { - (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _) - (owner, name, userName, issueId, _) - } - recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) ) - - // extract references and create refer comment - content.map { content => - createReferComment(owner, name, issue, content) - } - - // notifications - Notifier() match { - case f => - content foreach { - f.toNotify(repository, issueId, _){ - Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${ - if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}") - } - } - action foreach { - f.toNotify(repository, issueId, _){ - Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") - } - } - } - - issue -> commentId - } - } - } - - private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = { - defining(repository.owner, repository.name){ case (owner, repoName) => - val filterUser = Map(filter -> params.getOrElse("userName", "")) - val page = IssueSearchCondition.page(request) - val sessionKey = Keys.Session.Issues(owner, repoName) - - // retrieve search condition - val condition = session.putAndGet(sessionKey, - if(request.hasQueryString) IssueSearchCondition(request) - else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition()) - ) - - issues.html.list( - searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), - page, - (getCollaborators(owner, repoName) :+ owner).sorted, - getMilestones(owner, repoName), - getLabels(owner, repoName), - countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName), - countIssue(condition.copy(state = "closed"), filterUser, 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("created_by" -> x.userName), false, owner -> repoName)), - countIssueGroupByLabels(owner, repoName, condition, filterUser), - condition, - filter, - repository, - hasWritePermission(owner, repoName, context.loginAccount)) - } - } - -} +package app + +import jp.sf.amateras.scalatra.forms._ + +import service._ +import IssuesService._ +import util._ +import util.Implicits._ +import util.ControlUtil._ +import org.scalatra.Ok +import model.Issue + +class IssuesController extends IssuesControllerBase + with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService + with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator + +trait IssuesControllerBase extends ControllerBase { + self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService + with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator => + + case class IssueCreateForm(title: String, content: Option[String], + assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String]) + case class IssueEditForm(title: String, content: Option[String]) + case class CommentForm(issueId: Int, content: String) + case class IssueStateForm(issueId: Int, content: Option[String]) + + val issueCreateForm = mapping( + "title" -> trim(label("Title", text(required))), + "content" -> trim(optional(text())), + "assignedUserName" -> trim(optional(text())), + "milestoneId" -> trim(optional(number())), + "labelNames" -> trim(optional(text())) + )(IssueCreateForm.apply) + + val issueEditForm = mapping( + "title" -> trim(label("Title", text(required))), + "content" -> trim(optional(text())) + )(IssueEditForm.apply) + + val commentForm = mapping( + "issueId" -> label("Issue Id", number()), + "content" -> trim(label("Comment", text(required))) + )(CommentForm.apply) + + val issueStateForm = mapping( + "issueId" -> label("Issue Id", number()), + "content" -> trim(optional(text())) + )(IssueStateForm.apply) + + get("/:owner/:repository/issues")(referrersOnly { + searchIssues("all", _) + }) + + get("/:owner/:repository/issues/assigned/:userName")(referrersOnly { + searchIssues("assigned", _) + }) + + get("/:owner/:repository/issues/created_by/:userName")(referrersOnly { + searchIssues("created_by", _) + }) + + get("/:owner/:repository/issues/:id")(referrersOnly { repository => + defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) => + getIssue(owner, name, issueId) map { + issues.html.issue( + _, + getComments(owner, name, issueId.toInt), + getIssueLabels(owner, name, issueId.toInt), + (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, + getMilestonesWithIssueCount(owner, name), + getLabels(owner, name), + hasWritePermission(owner, name, context.loginAccount), + repository) + } getOrElse NotFound + } + }) + + get("/:owner/:repository/issues/new")(readableUsersOnly { repository => + defining(repository.owner, repository.name){ case (owner, name) => + issues.html.create( + (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, + getMilestones(owner, name), + getLabels(owner, name), + hasWritePermission(owner, name, context.loginAccount), + repository) + } + }) + + post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) => + defining(repository.owner, repository.name){ case (owner, name) => + val writable = hasWritePermission(owner, name, context.loginAccount) + val userName = context.loginAccount.get.userName + + // insert issue + val issueId = createIssue(owner, name, userName, form.title, form.content, + if(writable) form.assignedUserName else None, + if(writable) form.milestoneId else None) + + // insert labels + if(writable){ + form.labelNames.map { value => + val labels = getLabels(owner, name) + value.split(",").foreach { labelName => + labels.find(_.labelName == labelName).map { label => + registerIssueLabel(owner, name, issueId, label.labelId) + } + } + } + } + + // record activity + recordCreateIssueActivity(owner, name, userName, issueId, form.title) + + // extract references and create refer comment + getIssue(owner, name, issueId.toString).foreach { issue => + createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) + } + + // notifications + Notifier().toNotify(repository, issueId, form.content.getOrElse("")){ + Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") + } + + redirect(s"/${owner}/${name}/issues/${issueId}") + } + }) + + ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) => + defining(repository.owner, repository.name){ case (owner, name) => + getIssue(owner, name, params("id")).map { issue => + if(isEditable(owner, name, issue.openedUserName)){ + // update issue + updateIssue(owner, name, issue.issueId, form.title, form.content) + // extract references and create refer comment + createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) + + redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") + } else Unauthorized + } getOrElse NotFound + } + }) + + post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => + handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) => + redirect(s"/${repository.owner}/${repository.name}/${ + if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") + } getOrElse NotFound + }) + + post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) => + handleComment(form.issueId, form.content, repository)() map { case (issue, id) => + redirect(s"/${repository.owner}/${repository.name}/${ + if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") + } getOrElse NotFound + }) + + ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => + defining(repository.owner, repository.name){ case (owner, name) => + getComment(owner, name, params("id")).map { comment => + if(isEditable(owner, name, comment.commentedUserName)){ + updateComment(comment.commentId, form.content) + redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}") + } else Unauthorized + } getOrElse NotFound + } + }) + + ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository => + defining(repository.owner, repository.name){ case (owner, name) => + getComment(owner, name, params("id")).map { comment => + if(isEditable(owner, name, comment.commentedUserName)){ + Ok(deleteComment(comment.commentId)) + } else Unauthorized + } getOrElse NotFound + } + }) + + ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository => + getIssue(repository.owner, repository.name, params("id")) map { x => + if(isEditable(x.userName, x.repositoryName, x.openedUserName)){ + params.get("dataType") collect { + case t if t == "html" => issues.html.editissue( + x.title, x.content, x.issueId, x.userName, x.repositoryName) + } getOrElse { + contentType = formats("json") + org.json4s.jackson.Serialization.write( + Map("title" -> x.title, + "content" -> view.Markdown.toHtml(x.content getOrElse "No description given.", + repository, false, true) + )) + } + } else Unauthorized + } getOrElse NotFound + }) + + ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository => + getComment(repository.owner, repository.name, params("id")) map { x => + if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){ + params.get("dataType") collect { + case t if t == "html" => issues.html.editcomment( + x.content, x.commentId, x.userName, x.repositoryName) + } getOrElse { + contentType = formats("json") + org.json4s.jackson.Serialization.write( + Map("content" -> view.Markdown.toHtml(x.content, + repository, false, true) + )) + } + } else Unauthorized + } getOrElse NotFound + }) + + ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository => + defining(params("id").toInt){ issueId => + registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) + issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) + } + }) + + ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository => + defining(params("id").toInt){ issueId => + deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) + issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) + } + }) + + ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository => + updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName")) + Ok("updated") + }) + + ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository => + updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId")) + milestoneId("milestoneId").map { milestoneId => + getMilestonesWithIssueCount(repository.owner, repository.name) + .find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) => + issues.milestones.html.progress(openCount + closeCount, closeCount, false) + } getOrElse NotFound + } getOrElse Ok() + }) + + post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => + defining(params.get("value")){ action => + executeBatch(repository) { + handleComment(_, None, repository)( _ => action) + } + } + }) + + post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository => + params("value").toIntOpt.map{ labelId => + executeBatch(repository) { issueId => + getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse { + registerIssueLabel(repository.owner, repository.name, issueId, labelId) + } + } + } getOrElse NotFound + }) + + post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository => + defining(assignedUserName("value")){ value => + executeBatch(repository) { + updateAssignedUserName(repository.owner, repository.name, _, value) + } + } + }) + + post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository => + defining(milestoneId("value")){ value => + executeBatch(repository) { + updateMilestoneId(repository.owner, repository.name, _, value) + } + } + }) + + get("/:owner/:repository/_attached/:file")(referrersOnly { repository => + (Directory.getAttachedDir(repository.owner, repository.name) match { + case dir if(dir.exists && dir.isDirectory) => + dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file => + contentType = FileUtil.getMimeType(file.getName) + file + } + case _ => None + }) getOrElse NotFound + }) + + val assignedUserName = (key: String) => params.get(key) filter (_.trim != "") + 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 = + hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName + + private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = { + params("checked").split(',') map(_.toInt) foreach execute + redirect(s"/${repository.owner}/${repository.name}/issues") + } + + private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = { + StringUtil.extractIssueId(message).foreach { issueId => + if(getIssue(owner, repository, issueId).isDefined){ + createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, + fromIssue.issueId + ":" + fromIssue.title, "refer") + } + } + } + + /** + * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]] + */ + private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo) + (getAction: model.Issue => Option[String] = + p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = { + + defining(repository.owner, repository.name){ case (owner, name) => + val userName = context.loginAccount.get.userName + + getIssue(owner, name, issueId.toString) map { issue => + val (action, recordActivity) = + getAction(issue) + .collect { + case "close" => true -> (Some("close") -> + Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _)) + case "reopen" => false -> (Some("reopen") -> + Some(recordReopenIssueActivity _)) + } + .map { case (closed, t) => + updateClosed(owner, name, issueId, closed) + t + } + .getOrElse(None -> None) + + val commentId = content + .map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") ) + .getOrElse ( action.get.capitalize -> action.get ) + match { + case (content, action) => createComment(owner, name, userName, issueId, content, action) + } + + // record activity + content foreach { + (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _) + (owner, name, userName, issueId, _) + } + recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) ) + + // extract references and create refer comment + content.map { content => + createReferComment(owner, name, issue, content) + } + + // notifications + Notifier() match { + case f => + content foreach { + f.toNotify(repository, issueId, _){ + Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${ + if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}") + } + } + action foreach { + f.toNotify(repository, issueId, _){ + Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") + } + } + } + + issue -> commentId + } + } + } + + private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = { + defining(repository.owner, repository.name){ case (owner, repoName) => + val filterUser = Map(filter -> params.getOrElse("userName", "")) + val page = IssueSearchCondition.page(request) + val sessionKey = Keys.Session.Issues(owner, repoName) + + // retrieve search condition + val condition = session.putAndGet(sessionKey, + if(request.hasQueryString) IssueSearchCondition(request) + else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition()) + ) + + issues.html.list( + searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), + page, + (getCollaborators(owner, repoName) :+ owner).sorted, + getMilestones(owner, repoName), + getLabels(owner, repoName), + countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName), + countIssue(condition.copy(state = "closed"), filterUser, 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("created_by" -> x.userName), false, owner -> repoName)), + countIssueGroupByLabels(owner, repoName, condition, filterUser), + condition, + filter, + repository, + hasWritePermission(owner, repoName, context.loginAccount)) + } + } + +} diff --git a/src/main/scala/app/RepositorySettingsController.scala b/src/main/scala/app/RepositorySettingsController.scala index ed30d93..103e0ea 100644 --- a/src/main/scala/app/RepositorySettingsController.scala +++ b/src/main/scala/app/RepositorySettingsController.scala @@ -1,269 +1,269 @@ -package app - -import service._ -import util.Directory._ -import util.{LockUtil, UsersAuthenticator, OwnerAuthenticator} -import jp.sf.amateras.scalatra.forms._ -import org.apache.commons.io.FileUtils -import org.scalatra.i18n.Messages -import service.WebHookService.WebHookPayload -import util.JGitUtil.CommitInfo -import util.ControlUtil._ -import org.eclipse.jgit.api.Git - -class RepositorySettingsController extends RepositorySettingsControllerBase - with RepositoryService with AccountService with WebHookService - with OwnerAuthenticator with UsersAuthenticator - -trait RepositorySettingsControllerBase extends ControllerBase { - self: RepositoryService with AccountService with WebHookService - with OwnerAuthenticator with UsersAuthenticator => - - // for repository options - case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean) - - val optionsForm = mapping( - "repositoryName" -> trim(label("Description" , text(required, maxlength(40), identifier, renameRepositoryName))), - "description" -> trim(label("Description" , optional(text()))), - "defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))), - "isPrivate" -> trim(label("Repository Type", boolean())) - )(OptionsForm.apply) - - // for collaborator addition - case class CollaboratorForm(userName: String) - - val collaboratorForm = mapping( - "userName" -> trim(label("Username", text(required, collaborator))) - )(CollaboratorForm.apply) - - // for web hook url addition - case class WebHookForm(url: String) - - val webHookForm = mapping( - "url" -> trim(label("url", text(required, webHook))) - )(WebHookForm.apply) - - // for transfer ownership - case class TransferOwnerShipForm(newOwner: String) - - val transferForm = mapping( - "newOwner" -> trim(label("New owner", text(required, transferUser))) - )(TransferOwnerShipForm.apply) - - /** - * Redirect to the Options page. - */ - get("/:owner/:repository/settings")(ownerOnly { repository => - redirect(s"/${repository.owner}/${repository.name}/settings/options") - }) - - /** - * Display the Options page. - */ - get("/:owner/:repository/settings/options")(ownerOnly { - settings.html.options(_, flash.get("info")) - }) - - /** - * Save the repository options. - */ - post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) => - saveRepositoryOptions( - repository.owner, - repository.name, - form.description, - if(repository.branchList.isEmpty) "master" else form.defaultBranch, - repository.repository.parentUserName.map { _ => - repository.repository.isPrivate - } getOrElse form.isPrivate - ) - // Change repository name - if(repository.name != form.repositoryName){ - // Update database - renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName) - // Move git repository - defining(getRepositoryDir(repository.owner, repository.name)){ dir => - FileUtils.moveDirectory(dir, getRepositoryDir(repository.owner, form.repositoryName)) - } - // Move wiki repository - defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir => - FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName)) - } - } - flash += "info" -> "Repository settings has been updated." - redirect(s"/${repository.owner}/${form.repositoryName}/settings/options") - }) - - /** - * Display the Collaborators page. - */ - get("/:owner/:repository/settings/collaborators")(ownerOnly { repository => - settings.html.collaborators( - getCollaborators(repository.owner, repository.name), - getAccountByUserName(repository.owner).get.isGroupAccount, - repository) - }) - - /** - * Add the collaborator. - */ - post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) => - if(!getAccountByUserName(repository.owner).get.isGroupAccount){ - addCollaborator(repository.owner, repository.name, form.userName) - } - redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") - }) - - /** - * Add the collaborator. - */ - get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository => - if(!getAccountByUserName(repository.owner).get.isGroupAccount){ - removeCollaborator(repository.owner, repository.name, params("name")) - } - redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") - }) - - /** - * Display the web hook page. - */ - get("/:owner/:repository/settings/hooks")(ownerOnly { repository => - settings.html.hooks(getWebHookURLs(repository.owner, repository.name), repository, flash.get("info")) - }) - - /** - * Add the web hook URL. - */ - post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) => - addWebHookURL(repository.owner, repository.name, form.url) - redirect(s"/${repository.owner}/${repository.name}/settings/hooks") - }) - - /** - * Delete the web hook URL. - */ - get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository => - deleteWebHookURL(repository.owner, repository.name, params("url")) - redirect(s"/${repository.owner}/${repository.name}/settings/hooks") - }) - - /** - * Send the test request to registered web hook URLs. - */ - get("/:owner/:repository/settings/hooks/test")(ownerOnly { repository => - using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - import scala.collection.JavaConverters._ - val commits = git.log - .add(git.getRepository.resolve(repository.repository.defaultBranch)) - .setMaxCount(3) - .call.iterator.asScala.map(new CommitInfo(_)) - - getWebHookURLs(repository.owner, repository.name) match { - case webHookURLs if(webHookURLs.nonEmpty) => - for(ownerAccount <- getAccountByUserName(repository.owner)){ - callWebHook(repository.owner, repository.name, webHookURLs, - WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)) - } - case _ => - } - - flash += "info" -> "Test payload deployed!" - } - redirect(s"/${repository.owner}/${repository.name}/settings/hooks") - }) - - /** - * Display the danger zone. - */ - get("/:owner/:repository/settings/danger")(ownerOnly { - settings.html.danger(_) - }) - - /** - * Transfer repository ownership. - */ - post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) => - // Change repository owner - if(repository.owner != form.newOwner){ - LockUtil.lock(s"${repository.owner}/${repository.name}"){ - // Update database - renameRepository(repository.owner, repository.name, form.newOwner, repository.name) - // Move git repository - defining(getRepositoryDir(repository.owner, repository.name)){ dir => - FileUtils.moveDirectory(dir, getRepositoryDir(form.newOwner, repository.name)) - } - // Move wiki repository - defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir => - FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name)) - } - } - } - redirect(s"/${form.newOwner}/${repository.name}") - }) - - /** - * Delete the repository. - */ - post("/:owner/:repository/settings/delete")(ownerOnly { repository => - 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)) - } - redirect(s"/${repository.owner}") - }) - - /** - * Provides duplication check for web hook url. - */ - private def webHook: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = - getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.") - } - - /** - * Provides Constraint to validate the collaborator name. - */ - private def collaborator: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = - getAccountByUserName(value) match { - case None => Some("User does not exist.") - case Some(x) if(x.isGroupAccount) - => Some("User does not exist.") - case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName)) - => Some("User can access this repository already.") - case _ => None - } - } - - /** - * Duplicate check for the rename repository name. - */ - private def renameRepositoryName: Constraint = new Constraint(){ - override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = - params.get("repository").filter(_ != value).flatMap { _ => - params.get("owner").flatMap { userName => - getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.") - } - } - } - - /** - * Provides Constraint to validate the repository transfer user. - */ - private def transferUser: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = - getAccountByUserName(value) match { - case None => Some("User does not exist.") - case Some(x) => if(x.userName == params("owner")){ - Some("This is current repository owner.") - } else { - params.get("repository").flatMap { repositoryName => - getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map{ _ => "User already has same repository." } - } - } - } - } +package app + +import service._ +import util.Directory._ +import util.{LockUtil, UsersAuthenticator, OwnerAuthenticator} +import jp.sf.amateras.scalatra.forms._ +import org.apache.commons.io.FileUtils +import org.scalatra.i18n.Messages +import service.WebHookService.WebHookPayload +import util.JGitUtil.CommitInfo +import util.ControlUtil._ +import org.eclipse.jgit.api.Git + +class RepositorySettingsController extends RepositorySettingsControllerBase + with RepositoryService with AccountService with WebHookService + with OwnerAuthenticator with UsersAuthenticator + +trait RepositorySettingsControllerBase extends ControllerBase { + self: RepositoryService with AccountService with WebHookService + with OwnerAuthenticator with UsersAuthenticator => + + // for repository options + case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean) + + val optionsForm = mapping( + "repositoryName" -> trim(label("Description" , text(required, maxlength(40), identifier, renameRepositoryName))), + "description" -> trim(label("Description" , optional(text()))), + "defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))), + "isPrivate" -> trim(label("Repository Type", boolean())) + )(OptionsForm.apply) + + // for collaborator addition + case class CollaboratorForm(userName: String) + + val collaboratorForm = mapping( + "userName" -> trim(label("Username", text(required, collaborator))) + )(CollaboratorForm.apply) + + // for web hook url addition + case class WebHookForm(url: String) + + val webHookForm = mapping( + "url" -> trim(label("url", text(required, webHook))) + )(WebHookForm.apply) + + // for transfer ownership + case class TransferOwnerShipForm(newOwner: String) + + val transferForm = mapping( + "newOwner" -> trim(label("New owner", text(required, transferUser))) + )(TransferOwnerShipForm.apply) + + /** + * Redirect to the Options page. + */ + get("/:owner/:repository/settings")(ownerOnly { repository => + redirect(s"/${repository.owner}/${repository.name}/settings/options") + }) + + /** + * Display the Options page. + */ + get("/:owner/:repository/settings/options")(ownerOnly { + settings.html.options(_, flash.get("info")) + }) + + /** + * Save the repository options. + */ + post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) => + saveRepositoryOptions( + repository.owner, + repository.name, + form.description, + if(repository.branchList.isEmpty) "master" else form.defaultBranch, + repository.repository.parentUserName.map { _ => + repository.repository.isPrivate + } getOrElse form.isPrivate + ) + // Change repository name + if(repository.name != form.repositoryName){ + // Update database + renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName) + // Move git repository + defining(getRepositoryDir(repository.owner, repository.name)){ dir => + FileUtils.moveDirectory(dir, getRepositoryDir(repository.owner, form.repositoryName)) + } + // Move wiki repository + defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir => + FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName)) + } + } + flash += "info" -> "Repository settings has been updated." + redirect(s"/${repository.owner}/${form.repositoryName}/settings/options") + }) + + /** + * Display the Collaborators page. + */ + get("/:owner/:repository/settings/collaborators")(ownerOnly { repository => + settings.html.collaborators( + getCollaborators(repository.owner, repository.name), + getAccountByUserName(repository.owner).get.isGroupAccount, + repository) + }) + + /** + * Add the collaborator. + */ + post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) => + if(!getAccountByUserName(repository.owner).get.isGroupAccount){ + addCollaborator(repository.owner, repository.name, form.userName) + } + redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") + }) + + /** + * Add the collaborator. + */ + get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository => + if(!getAccountByUserName(repository.owner).get.isGroupAccount){ + removeCollaborator(repository.owner, repository.name, params("name")) + } + redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") + }) + + /** + * Display the web hook page. + */ + get("/:owner/:repository/settings/hooks")(ownerOnly { repository => + settings.html.hooks(getWebHookURLs(repository.owner, repository.name), repository, flash.get("info")) + }) + + /** + * Add the web hook URL. + */ + post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) => + addWebHookURL(repository.owner, repository.name, form.url) + redirect(s"/${repository.owner}/${repository.name}/settings/hooks") + }) + + /** + * Delete the web hook URL. + */ + get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository => + deleteWebHookURL(repository.owner, repository.name, params("url")) + redirect(s"/${repository.owner}/${repository.name}/settings/hooks") + }) + + /** + * Send the test request to registered web hook URLs. + */ + get("/:owner/:repository/settings/hooks/test")(ownerOnly { repository => + using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => + import scala.collection.JavaConverters._ + val commits = git.log + .add(git.getRepository.resolve(repository.repository.defaultBranch)) + .setMaxCount(3) + .call.iterator.asScala.map(new CommitInfo(_)) + + getWebHookURLs(repository.owner, repository.name) match { + case webHookURLs if(webHookURLs.nonEmpty) => + for(ownerAccount <- getAccountByUserName(repository.owner)){ + callWebHook(repository.owner, repository.name, webHookURLs, + WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)) + } + case _ => + } + + flash += "info" -> "Test payload deployed!" + } + redirect(s"/${repository.owner}/${repository.name}/settings/hooks") + }) + + /** + * Display the danger zone. + */ + get("/:owner/:repository/settings/danger")(ownerOnly { + settings.html.danger(_) + }) + + /** + * Transfer repository ownership. + */ + post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) => + // Change repository owner + if(repository.owner != form.newOwner){ + LockUtil.lock(s"${repository.owner}/${repository.name}"){ + // Update database + renameRepository(repository.owner, repository.name, form.newOwner, repository.name) + // Move git repository + defining(getRepositoryDir(repository.owner, repository.name)){ dir => + FileUtils.moveDirectory(dir, getRepositoryDir(form.newOwner, repository.name)) + } + // Move wiki repository + defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir => + FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name)) + } + } + } + redirect(s"/${form.newOwner}/${repository.name}") + }) + + /** + * Delete the repository. + */ + post("/:owner/:repository/settings/delete")(ownerOnly { repository => + 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)) + } + redirect(s"/${repository.owner}") + }) + + /** + * Provides duplication check for web hook url. + */ + private def webHook: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = + getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.") + } + + /** + * Provides Constraint to validate the collaborator name. + */ + private def collaborator: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = + getAccountByUserName(value) match { + case None => Some("User does not exist.") + case Some(x) if(x.isGroupAccount) + => Some("User does not exist.") + case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName)) + => Some("User can access this repository already.") + case _ => None + } + } + + /** + * Duplicate check for the rename repository name. + */ + private def renameRepositoryName: Constraint = new Constraint(){ + override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = + params.get("repository").filter(_ != value).flatMap { _ => + params.get("owner").flatMap { userName => + getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.") + } + } + } + + /** + * Provides Constraint to validate the repository transfer user. + */ + private def transferUser: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = + getAccountByUserName(value) match { + case None => Some("User does not exist.") + case Some(x) => if(x.userName == params("owner")){ + Some("This is current repository owner.") + } else { + params.get("repository").flatMap { repositoryName => + getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map{ _ => "User already has same repository." } + } + } + } + } } \ No newline at end of file diff --git a/src/main/scala/model/Account.scala b/src/main/scala/model/Account.scala index 8c3ff27..93d0f68 100644 --- a/src/main/scala/model/Account.scala +++ b/src/main/scala/model/Account.scala @@ -1,34 +1,34 @@ -package model - -import scala.slick.driver.H2Driver.simple._ - -object Accounts extends Table[Account]("ACCOUNT") { - def userName = column[String]("USER_NAME", O PrimaryKey) - def fullName = column[String]("FULL_NAME") - def mailAddress = column[String]("MAIL_ADDRESS") - def password = column[String]("PASSWORD") - def isAdmin = column[Boolean]("ADMINISTRATOR") - def url = column[String]("URL") - def registeredDate = column[java.util.Date]("REGISTERED_DATE") - def updatedDate = column[java.util.Date]("UPDATED_DATE") - def lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE") - def image = column[String]("IMAGE") - def groupAccount = column[Boolean]("GROUP_ACCOUNT") - def removed = column[Boolean]("REMOVED") - def * = userName ~ fullName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? ~ groupAccount ~ removed <> (Account, Account.unapply _) -} - -case class Account( - userName: String, - fullName: String, - mailAddress: String, - password: String, - isAdmin: Boolean, - url: Option[String], - registeredDate: java.util.Date, - updatedDate: java.util.Date, - lastLoginDate: Option[java.util.Date], - image: Option[String], - isGroupAccount: Boolean, - isRemoved: Boolean -) +package model + +import scala.slick.driver.H2Driver.simple._ + +object Accounts extends Table[Account]("ACCOUNT") { + def userName = column[String]("USER_NAME", O PrimaryKey) + def fullName = column[String]("FULL_NAME") + def mailAddress = column[String]("MAIL_ADDRESS") + def password = column[String]("PASSWORD") + def isAdmin = column[Boolean]("ADMINISTRATOR") + def url = column[String]("URL") + def registeredDate = column[java.util.Date]("REGISTERED_DATE") + def updatedDate = column[java.util.Date]("UPDATED_DATE") + def lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE") + def image = column[String]("IMAGE") + def groupAccount = column[Boolean]("GROUP_ACCOUNT") + def removed = column[Boolean]("REMOVED") + def * = userName ~ fullName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? ~ groupAccount ~ removed <> (Account, Account.unapply _) +} + +case class Account( + userName: String, + fullName: String, + mailAddress: String, + password: String, + isAdmin: Boolean, + url: Option[String], + registeredDate: java.util.Date, + updatedDate: java.util.Date, + lastLoginDate: Option[java.util.Date], + image: Option[String], + isGroupAccount: Boolean, + isRemoved: Boolean +) diff --git a/src/main/scala/model/BasicTemplate.scala b/src/main/scala/model/BasicTemplate.scala index 2d92e64..3327f06 100644 --- a/src/main/scala/model/BasicTemplate.scala +++ b/src/main/scala/model/BasicTemplate.scala @@ -1,44 +1,44 @@ -package model - -import scala.slick.driver.H2Driver.simple._ - -protected[model] trait BasicTemplate { self: Table[_] => - def userName = column[String]("USER_NAME") - def repositoryName = column[String]("REPOSITORY_NAME") - - def byRepository(owner: String, repository: String) = - (userName is owner.bind) && (repositoryName is repository.bind) - - def byRepository(userName: Column[String], repositoryName: Column[String]) = - (this.userName is userName) && (this.repositoryName is repositoryName) -} - -protected[model] trait IssueTemplate extends BasicTemplate { self: Table[_] => - def issueId = column[Int]("ISSUE_ID") - - def byIssue(owner: String, repository: String, issueId: Int) = - byRepository(owner, repository) && (this.issueId is issueId.bind) - - def byIssue(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = - byRepository(userName, repositoryName) && (this.issueId is issueId) -} - -protected[model] trait LabelTemplate extends BasicTemplate { self: Table[_] => - def labelId = column[Int]("LABEL_ID") - - def byLabel(owner: String, repository: String, labelId: Int) = - byRepository(owner, repository) && (this.labelId is labelId.bind) - - def byLabel(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) = - byRepository(userName, repositoryName) && (this.labelId is labelId) -} - -protected[model] trait MilestoneTemplate extends BasicTemplate { self: Table[_] => - def milestoneId = column[Int]("MILESTONE_ID") - - def byMilestone(owner: String, repository: String, milestoneId: Int) = - byRepository(owner, repository) && (this.milestoneId is milestoneId.bind) - - def byMilestone(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) = - byRepository(userName, repositoryName) && (this.milestoneId is milestoneId) +package model + +import scala.slick.driver.H2Driver.simple._ + +protected[model] trait BasicTemplate { self: Table[_] => + def userName = column[String]("USER_NAME") + def repositoryName = column[String]("REPOSITORY_NAME") + + def byRepository(owner: String, repository: String) = + (userName is owner.bind) && (repositoryName is repository.bind) + + def byRepository(userName: Column[String], repositoryName: Column[String]) = + (this.userName is userName) && (this.repositoryName is repositoryName) +} + +protected[model] trait IssueTemplate extends BasicTemplate { self: Table[_] => + def issueId = column[Int]("ISSUE_ID") + + def byIssue(owner: String, repository: String, issueId: Int) = + byRepository(owner, repository) && (this.issueId is issueId.bind) + + def byIssue(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = + byRepository(userName, repositoryName) && (this.issueId is issueId) +} + +protected[model] trait LabelTemplate extends BasicTemplate { self: Table[_] => + def labelId = column[Int]("LABEL_ID") + + def byLabel(owner: String, repository: String, labelId: Int) = + byRepository(owner, repository) && (this.labelId is labelId.bind) + + def byLabel(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) = + byRepository(userName, repositoryName) && (this.labelId is labelId) +} + +protected[model] trait MilestoneTemplate extends BasicTemplate { self: Table[_] => + def milestoneId = column[Int]("MILESTONE_ID") + + def byMilestone(owner: String, repository: String, milestoneId: Int) = + byRepository(owner, repository) && (this.milestoneId is milestoneId.bind) + + def byMilestone(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) = + byRepository(userName, repositoryName) && (this.milestoneId is milestoneId) } \ No newline at end of file diff --git a/src/main/scala/model/Issue.scala b/src/main/scala/model/Issue.scala index d5ce8a3..c44407e 100644 --- a/src/main/scala/model/Issue.scala +++ b/src/main/scala/model/Issue.scala @@ -1,41 +1,41 @@ -package model - -import scala.slick.driver.H2Driver.simple._ - -object IssueId extends Table[(String, String, Int)]("ISSUE_ID") with IssueTemplate { - def * = userName ~ repositoryName ~ issueId - def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository) -} - -object IssueOutline extends Table[(String, String, Int, Int)]("ISSUE_OUTLINE_VIEW") with IssueTemplate { - def commentCount = column[Int]("COMMENT_COUNT") - def * = userName ~ repositoryName ~ issueId ~ commentCount -} - -object Issues extends Table[Issue]("ISSUE") with IssueTemplate with MilestoneTemplate { - def openedUserName = column[String]("OPENED_USER_NAME") - def assignedUserName = column[String]("ASSIGNED_USER_NAME") - def title = column[String]("TITLE") - def content = column[String]("CONTENT") - def closed = column[Boolean]("CLOSED") - def registeredDate = column[java.util.Date]("REGISTERED_DATE") - def updatedDate = column[java.util.Date]("UPDATED_DATE") - def pullRequest = column[Boolean]("PULL_REQUEST") - def * = userName ~ repositoryName ~ issueId ~ openedUserName ~ milestoneId.? ~ assignedUserName.? ~ title ~ content.? ~ closed ~ registeredDate ~ updatedDate ~ pullRequest <> (Issue, Issue.unapply _) - - def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId) -} - -case class Issue( - userName: String, - repositoryName: String, - issueId: Int, - openedUserName: String, - milestoneId: Option[Int], - assignedUserName: Option[String], - title: String, - content: Option[String], - closed: Boolean, - registeredDate: java.util.Date, - updatedDate: java.util.Date, +package model + +import scala.slick.driver.H2Driver.simple._ + +object IssueId extends Table[(String, String, Int)]("ISSUE_ID") with IssueTemplate { + def * = userName ~ repositoryName ~ issueId + def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository) +} + +object IssueOutline extends Table[(String, String, Int, Int)]("ISSUE_OUTLINE_VIEW") with IssueTemplate { + def commentCount = column[Int]("COMMENT_COUNT") + def * = userName ~ repositoryName ~ issueId ~ commentCount +} + +object Issues extends Table[Issue]("ISSUE") with IssueTemplate with MilestoneTemplate { + def openedUserName = column[String]("OPENED_USER_NAME") + def assignedUserName = column[String]("ASSIGNED_USER_NAME") + def title = column[String]("TITLE") + def content = column[String]("CONTENT") + def closed = column[Boolean]("CLOSED") + def registeredDate = column[java.util.Date]("REGISTERED_DATE") + def updatedDate = column[java.util.Date]("UPDATED_DATE") + def pullRequest = column[Boolean]("PULL_REQUEST") + def * = userName ~ repositoryName ~ issueId ~ openedUserName ~ milestoneId.? ~ assignedUserName.? ~ title ~ content.? ~ closed ~ registeredDate ~ updatedDate ~ pullRequest <> (Issue, Issue.unapply _) + + def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId) +} + +case class Issue( + userName: String, + repositoryName: String, + issueId: Int, + openedUserName: String, + milestoneId: Option[Int], + assignedUserName: Option[String], + title: String, + content: Option[String], + closed: Boolean, + registeredDate: java.util.Date, + updatedDate: java.util.Date, isPullRequest: Boolean) \ No newline at end of file diff --git a/src/main/scala/model/IssueComment.scala b/src/main/scala/model/IssueComment.scala index f9fd983..0452b5d 100644 --- a/src/main/scala/model/IssueComment.scala +++ b/src/main/scala/model/IssueComment.scala @@ -1,28 +1,28 @@ -package model - -import scala.slick.driver.H2Driver.simple._ - -object IssueComments extends Table[IssueComment]("ISSUE_COMMENT") with IssueTemplate { - def commentId = column[Int]("COMMENT_ID", O AutoInc) - def action = column[String]("ACTION") - def commentedUserName = column[String]("COMMENTED_USER_NAME") - def content = column[String]("CONTENT") - def registeredDate = column[java.util.Date]("REGISTERED_DATE") - def updatedDate = column[java.util.Date]("UPDATED_DATE") - def * = userName ~ repositoryName ~ issueId ~ commentId ~ action ~ commentedUserName ~ content ~ registeredDate ~ updatedDate <> (IssueComment, IssueComment.unapply _) - - def autoInc = userName ~ repositoryName ~ issueId ~ action ~ commentedUserName ~ content ~ registeredDate ~ updatedDate returning commentId - def byPrimaryKey(commentId: Int) = this.commentId is commentId.bind -} - -case class IssueComment( - userName: String, - repositoryName: String, - issueId: Int, - commentId: Int, - action: String, - commentedUserName: String, - content: String, - registeredDate: java.util.Date, - updatedDate: java.util.Date +package model + +import scala.slick.driver.H2Driver.simple._ + +object IssueComments extends Table[IssueComment]("ISSUE_COMMENT") with IssueTemplate { + def commentId = column[Int]("COMMENT_ID", O AutoInc) + def action = column[String]("ACTION") + def commentedUserName = column[String]("COMMENTED_USER_NAME") + def content = column[String]("CONTENT") + def registeredDate = column[java.util.Date]("REGISTERED_DATE") + def updatedDate = column[java.util.Date]("UPDATED_DATE") + def * = userName ~ repositoryName ~ issueId ~ commentId ~ action ~ commentedUserName ~ content ~ registeredDate ~ updatedDate <> (IssueComment, IssueComment.unapply _) + + def autoInc = userName ~ repositoryName ~ issueId ~ action ~ commentedUserName ~ content ~ registeredDate ~ updatedDate returning commentId + def byPrimaryKey(commentId: Int) = this.commentId is commentId.bind +} + +case class IssueComment( + userName: String, + repositoryName: String, + issueId: Int, + commentId: Int, + action: String, + commentedUserName: String, + content: String, + registeredDate: java.util.Date, + updatedDate: java.util.Date ) \ No newline at end of file diff --git a/src/main/scala/model/package.scala b/src/main/scala/model/package.scala index 3280c35..f80d679 100644 --- a/src/main/scala/model/package.scala +++ b/src/main/scala/model/package.scala @@ -1,20 +1,20 @@ -package object model { - import scala.slick.driver.BasicDriver.Implicit._ - import scala.slick.lifted.{Column, MappedTypeMapper} - - // java.util.Date TypeMapper - implicit val dateTypeMapper = MappedTypeMapper.base[java.util.Date, java.sql.Timestamp]( - d => new java.sql.Timestamp(d.getTime), - t => new java.util.Date(t.getTime) - ) - - implicit class RichColumn(c1: Column[Boolean]){ - def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1 - } - - /** - * Returns system date. - */ - def currentDate = new java.util.Date() - +package object model { + import scala.slick.driver.BasicDriver.Implicit._ + import scala.slick.lifted.{Column, MappedTypeMapper} + + // java.util.Date TypeMapper + implicit val dateTypeMapper = MappedTypeMapper.base[java.util.Date, java.sql.Timestamp]( + d => new java.sql.Timestamp(d.getTime), + t => new java.util.Date(t.getTime) + ) + + implicit class RichColumn(c1: Column[Boolean]){ + def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1 + } + + /** + * Returns system date. + */ + def currentDate = new java.util.Date() + } \ No newline at end of file diff --git a/src/main/scala/service/IssuesService.scala b/src/main/scala/service/IssuesService.scala index 590e300..ea8df53 100644 --- a/src/main/scala/service/IssuesService.scala +++ b/src/main/scala/service/IssuesService.scala @@ -1,380 +1,380 @@ -package service - -import scala.slick.driver.H2Driver.simple._ -import Database.threadLocalSession -import scala.slick.jdbc.{StaticQuery => Q} -import Q.interpolation - -import model._ -import util.Implicits._ -import util.StringUtil._ - -trait IssuesService { - import IssuesService._ - - def getIssue(owner: String, repository: String, issueId: String) = - if (issueId forall (_.isDigit)) - Query(Issues) filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption - else None - - def getComments(owner: String, repository: String, issueId: Int) = - Query(IssueComments) filter (_.byIssue(owner, repository, issueId)) list - - def getComment(owner: String, repository: String, commentId: String) = - if (commentId forall (_.isDigit)) - Query(IssueComments) filter { t => - t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository) - } firstOption - else None - - def getIssueLabels(owner: String, repository: String, issueId: Int) = - IssueLabels - .innerJoin(Labels).on { (t1, t2) => - t1.byLabel(t2.userName, t2.repositoryName, t2.labelId) - } - .filter ( _._1.byIssue(owner, repository, issueId) ) - .map ( _._2 ) - .list - - def getIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) = - Query(IssueLabels) filter (_.byPrimaryKey(owner, repository, issueId, labelId)) firstOption - - /** - * Returns the count of the search result against issues. - * - * @param condition the search condition - * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name) - * @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request. - * @param repos Tuple of the repository owner and the repository name - * @return the count of the search result - */ - def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, - repos: (String, String)*): Int = - Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first - /** - * Returns the Map which contains issue count for each labels. - * - * @param owner the repository owner - * @param repository the repository name - * @param condition the search condition - * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name) - * @return the Map which contains issue count for each labels (key is label name, value is issue count) - */ - def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition, - filterUser: Map[String, String]): Map[String, Int] = { - - searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false) - .innerJoin(IssueLabels).on { (t1, t2) => - t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) - } - .innerJoin(Labels).on { case ((t1, t2), t3) => - t2.byLabel(t3.userName, t3.repositoryName, t3.labelId) - } - .groupBy { case ((t1, t2), t3) => - t3.labelName - } - .map { case (labelName, t) => - labelName ~ t.length - } - .toMap - } - /** - * Returns list which contains issue count for each repository. - * If the issue does not exist, its repository is not included in the result. - * - * @param condition the search condition - * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name) - * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request. - * @param repos Tuple of the repository owner and the repository name - * @return list which contains issue count for each repository - */ - def countIssueGroupByRepository( - condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, - repos: (String, String)*): List[(String, String, Int)] = { - searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest) - .groupBy { t => - t.userName ~ t.repositoryName - } - .map { case (repo, t) => - repo ~ t.length - } - .sortBy(_._3 desc) - .list - } - - /** - * Returns the search result against issues. - * - * @param condition the search condition - * @param filterUser the filter user name (key is "all", "assigned", "created_by" or "not_created_by", value is the user name) - * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request. - * @param offset the offset for pagination - * @param limit the limit for pagination - * @param repos Tuple of the repository owner and the repository name - * @return the search result (list of tuples which contain issue, labels and comment count) - */ - def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, - offset: Int, limit: Int, repos: (String, String)*): List[(Issue, List[Label], Int)] = { - - // get issues and comment count and labels - searchIssueQuery(repos, condition, filterUser, onlyPullRequest) - .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } - .sortBy { case (t1, t2) => - (condition.sort match { - case "created" => t1.registeredDate - case "comments" => t2.commentCount - case "updated" => t1.updatedDate - }) match { - case sort => condition.direction match { - case "asc" => sort asc - case "desc" => sort desc - } - } - } - .drop(offset).take(limit) - .leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } - .leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) } - .map { case (((t1, t2), t3), t4) => - (t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?) - } - .list - .splitWith { (c1, c2) => - c1._1.userName == c2._1.userName && - c1._1.repositoryName == c2._1.repositoryName && - c1._1.issueId == c2._1.issueId - } - .map { issues => issues.head match { - case (issue, commentCount, _,_,_) => - (issue, - issues.flatMap { t => t._3.map ( - Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get) - )} toList, - commentCount) - }} toList - } - - /** - * Assembles query for conditional issue searching. - */ - private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, - filterUser: Map[String, String], onlyPullRequest: Boolean) = - Query(Issues) filter { t1 => - condition.repo - .map { _.split('/') match { case array => Seq(array(0) -> array(1)) } } - .getOrElse (repos) - .map { case (owner, repository) => t1.byRepository(owner, repository) } - .foldLeft[Column[Boolean]](false) ( _ || _ ) && - (t1.closed is (condition.state == "closed").bind) && - (t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) && - (t1.milestoneId isNull, condition.milestoneId == Some(None)) && - (t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) && - (t1.openedUserName is filterUser("created_by").bind, filterUser.get("created_by").isDefined) && - (t1.openedUserName isNot filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) && - (t1.pullRequest is true.bind, onlyPullRequest) && - (IssueLabels filter { t2 => - (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && - (t2.labelId in - (Labels filter { t3 => - (t3.byRepository(t1.userName, t1.repositoryName)) && - (t3.labelName inSetBind condition.labels) - } map(_.labelId))) - } exists, condition.labels.nonEmpty) - } - - def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String], - assignedUserName: Option[String], milestoneId: Option[Int], isPullRequest: Boolean = false) = - // next id number - sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int] - .firstOption.filter { id => - Issues insert Issue( - owner, - repository, - id, - loginUser, - milestoneId, - assignedUserName, - title, - content, - false, - currentDate, - currentDate, - isPullRequest) - - // increment issue id - IssueId - .filter (_.byPrimaryKey(owner, repository)) - .map (_.issueId) - .update (id) > 0 - } get - - def registerIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) = - IssueLabels insert (IssueLabel(owner, repository, issueId, labelId)) - - def deleteIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) = - IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete - - def createComment(owner: String, repository: String, loginUser: String, - issueId: Int, content: String, action: String) = - IssueComments.autoInc insert ( - owner, - repository, - issueId, - action, - loginUser, - content, - currentDate, - currentDate) - - def updateIssue(owner: String, repository: String, issueId: Int, - title: String, content: Option[String]) = - Issues - .filter (_.byPrimaryKey(owner, repository, issueId)) - .map { t => - t.title ~ t.content.? ~ t.updatedDate - } - .update (title, content, currentDate) - - def updateAssignedUserName(owner: String, repository: String, issueId: Int, assignedUserName: Option[String]) = - Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName) - - def updateMilestoneId(owner: String, repository: String, issueId: Int, milestoneId: Option[Int]) = - Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId) - - def updateComment(commentId: Int, content: String) = - IssueComments - .filter (_.byPrimaryKey(commentId)) - .map { t => - t.content ~ t.updatedDate - } - .update (content, currentDate) - - def deleteComment(commentId: Int) = - IssueComments filter (_.byPrimaryKey(commentId)) delete - - def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean) = - Issues - .filter (_.byPrimaryKey(owner, repository, issueId)) - .map { t => - t.closed ~ t.updatedDate - } - .update (closed, currentDate) - - /** - * Search issues by keyword. - * - * @param owner the repository owner - * @param repository the repository name - * @param query the keywords separated by whitespace. - * @return issues with comment count and matched content of issue or comment - */ - def searchIssuesByKeyword(owner: String, repository: String, query: String): List[(Issue, Int, String)] = { - import scala.slick.driver.H2Driver.likeEncode - val keywords = splitWords(query.toLowerCase) - - // Search Issue - val issues = Issues - .innerJoin(IssueOutline).on { case (t1, t2) => - t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) - } - .filter { case (t1, t2) => - keywords.map { keyword => - (t1.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) || - (t1.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) - } .reduceLeft(_ && _) - } - .map { case (t1, t2) => - (t1, 0, t1.content.?, t2.commentCount) - } - - // Search IssueComment - val comments = IssueComments - .innerJoin(Issues).on { case (t1, t2) => - t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) - } - .innerJoin(IssueOutline).on { case ((t1, t2), t3) => - t2.byIssue(t3.userName, t3.repositoryName, t3.issueId) - } - .filter { case ((t1, t2), t3) => - keywords.map { query => - t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^') - }.reduceLeft(_ && _) - } - .map { case ((t1, t2), t3) => - (t2, t1.commentId, t1.content.?, t3.commentCount) - } - - issues.union(comments).sortBy { case (issue, commentId, _, _) => - issue.issueId ~ commentId - }.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) => - issue1.issueId == issue2.issueId - }.map { _.head match { - case (issue, _, content, commentCount) => (issue, commentCount, content.getOrElse("")) - } - }.toList - } - - def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String) = { - extractCloseId(message).foreach { issueId => - for(issue <- getIssue(owner, repository, issueId) if !issue.closed){ - createComment(owner, repository, userName, issue.issueId, "Close", "close") - updateClosed(owner, repository, issue.issueId, true) - } - } - } -} - -object IssuesService { - import javax.servlet.http.HttpServletRequest - - val IssueLimit = 30 - - case class IssueSearchCondition( - labels: Set[String] = Set.empty, - milestoneId: Option[Option[Int]] = None, - repo: Option[String] = None, - state: String = "open", - sort: String = "created", - direction: String = "desc"){ - - def toURL: String = - "?" + List( - if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))), - milestoneId.map { id => "milestone=" + (id match { - case Some(x) => x.toString - case None => "none" - })}, - repo.map("for=" + urlEncode(_)), - Some("state=" + urlEncode(state)), - Some("sort=" + urlEncode(sort)), - Some("direction=" + urlEncode(direction))).flatten.mkString("&") - - } - - object IssueSearchCondition { - - private def param(request: HttpServletRequest, name: String, allow: Seq[String] = Nil): Option[String] = { - val value = request.getParameter(name) - if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value) - } - - def apply(request: HttpServletRequest): IssueSearchCondition = - IssueSearchCondition( - param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty), - param(request, "milestone").map{ - case "none" => None - case x => x.toIntOpt - }, - param(request, "for"), - param(request, "state", Seq("open", "closed")).getOrElse("open"), - param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"), - param(request, "direction", Seq("asc", "desc")).getOrElse("desc")) - - def page(request: HttpServletRequest) = try { - val i = param(request, "page").getOrElse("1").toInt - if(i <= 0) 1 else i - } catch { - case e: NumberFormatException => 1 - } - } - -} +package service + +import scala.slick.driver.H2Driver.simple._ +import Database.threadLocalSession +import scala.slick.jdbc.{StaticQuery => Q} +import Q.interpolation + +import model._ +import util.Implicits._ +import util.StringUtil._ + +trait IssuesService { + import IssuesService._ + + def getIssue(owner: String, repository: String, issueId: String) = + if (issueId forall (_.isDigit)) + Query(Issues) filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption + else None + + def getComments(owner: String, repository: String, issueId: Int) = + Query(IssueComments) filter (_.byIssue(owner, repository, issueId)) list + + def getComment(owner: String, repository: String, commentId: String) = + if (commentId forall (_.isDigit)) + Query(IssueComments) filter { t => + t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository) + } firstOption + else None + + def getIssueLabels(owner: String, repository: String, issueId: Int) = + IssueLabels + .innerJoin(Labels).on { (t1, t2) => + t1.byLabel(t2.userName, t2.repositoryName, t2.labelId) + } + .filter ( _._1.byIssue(owner, repository, issueId) ) + .map ( _._2 ) + .list + + def getIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) = + Query(IssueLabels) filter (_.byPrimaryKey(owner, repository, issueId, labelId)) firstOption + + /** + * Returns the count of the search result against issues. + * + * @param condition the search condition + * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name) + * @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request. + * @param repos Tuple of the repository owner and the repository name + * @return the count of the search result + */ + def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, + repos: (String, String)*): Int = + Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first + /** + * Returns the Map which contains issue count for each labels. + * + * @param owner the repository owner + * @param repository the repository name + * @param condition the search condition + * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name) + * @return the Map which contains issue count for each labels (key is label name, value is issue count) + */ + def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition, + filterUser: Map[String, String]): Map[String, Int] = { + + searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false) + .innerJoin(IssueLabels).on { (t1, t2) => + t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) + } + .innerJoin(Labels).on { case ((t1, t2), t3) => + t2.byLabel(t3.userName, t3.repositoryName, t3.labelId) + } + .groupBy { case ((t1, t2), t3) => + t3.labelName + } + .map { case (labelName, t) => + labelName ~ t.length + } + .toMap + } + /** + * Returns list which contains issue count for each repository. + * If the issue does not exist, its repository is not included in the result. + * + * @param condition the search condition + * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name) + * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request. + * @param repos Tuple of the repository owner and the repository name + * @return list which contains issue count for each repository + */ + def countIssueGroupByRepository( + condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, + repos: (String, String)*): List[(String, String, Int)] = { + searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest) + .groupBy { t => + t.userName ~ t.repositoryName + } + .map { case (repo, t) => + repo ~ t.length + } + .sortBy(_._3 desc) + .list + } + + /** + * Returns the search result against issues. + * + * @param condition the search condition + * @param filterUser the filter user name (key is "all", "assigned", "created_by" or "not_created_by", value is the user name) + * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request. + * @param offset the offset for pagination + * @param limit the limit for pagination + * @param repos Tuple of the repository owner and the repository name + * @return the search result (list of tuples which contain issue, labels and comment count) + */ + def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, + offset: Int, limit: Int, repos: (String, String)*): List[(Issue, List[Label], Int)] = { + + // get issues and comment count and labels + searchIssueQuery(repos, condition, filterUser, onlyPullRequest) + .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } + .sortBy { case (t1, t2) => + (condition.sort match { + case "created" => t1.registeredDate + case "comments" => t2.commentCount + case "updated" => t1.updatedDate + }) match { + case sort => condition.direction match { + case "asc" => sort asc + case "desc" => sort desc + } + } + } + .drop(offset).take(limit) + .leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } + .leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) } + .map { case (((t1, t2), t3), t4) => + (t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?) + } + .list + .splitWith { (c1, c2) => + c1._1.userName == c2._1.userName && + c1._1.repositoryName == c2._1.repositoryName && + c1._1.issueId == c2._1.issueId + } + .map { issues => issues.head match { + case (issue, commentCount, _,_,_) => + (issue, + issues.flatMap { t => t._3.map ( + Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get) + )} toList, + commentCount) + }} toList + } + + /** + * Assembles query for conditional issue searching. + */ + private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, + filterUser: Map[String, String], onlyPullRequest: Boolean) = + Query(Issues) filter { t1 => + condition.repo + .map { _.split('/') match { case array => Seq(array(0) -> array(1)) } } + .getOrElse (repos) + .map { case (owner, repository) => t1.byRepository(owner, repository) } + .foldLeft[Column[Boolean]](false) ( _ || _ ) && + (t1.closed is (condition.state == "closed").bind) && + (t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) && + (t1.milestoneId isNull, condition.milestoneId == Some(None)) && + (t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) && + (t1.openedUserName is filterUser("created_by").bind, filterUser.get("created_by").isDefined) && + (t1.openedUserName isNot filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) && + (t1.pullRequest is true.bind, onlyPullRequest) && + (IssueLabels filter { t2 => + (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && + (t2.labelId in + (Labels filter { t3 => + (t3.byRepository(t1.userName, t1.repositoryName)) && + (t3.labelName inSetBind condition.labels) + } map(_.labelId))) + } exists, condition.labels.nonEmpty) + } + + def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String], + assignedUserName: Option[String], milestoneId: Option[Int], isPullRequest: Boolean = false) = + // next id number + sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int] + .firstOption.filter { id => + Issues insert Issue( + owner, + repository, + id, + loginUser, + milestoneId, + assignedUserName, + title, + content, + false, + currentDate, + currentDate, + isPullRequest) + + // increment issue id + IssueId + .filter (_.byPrimaryKey(owner, repository)) + .map (_.issueId) + .update (id) > 0 + } get + + def registerIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) = + IssueLabels insert (IssueLabel(owner, repository, issueId, labelId)) + + def deleteIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) = + IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete + + def createComment(owner: String, repository: String, loginUser: String, + issueId: Int, content: String, action: String) = + IssueComments.autoInc insert ( + owner, + repository, + issueId, + action, + loginUser, + content, + currentDate, + currentDate) + + def updateIssue(owner: String, repository: String, issueId: Int, + title: String, content: Option[String]) = + Issues + .filter (_.byPrimaryKey(owner, repository, issueId)) + .map { t => + t.title ~ t.content.? ~ t.updatedDate + } + .update (title, content, currentDate) + + def updateAssignedUserName(owner: String, repository: String, issueId: Int, assignedUserName: Option[String]) = + Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName) + + def updateMilestoneId(owner: String, repository: String, issueId: Int, milestoneId: Option[Int]) = + Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId) + + def updateComment(commentId: Int, content: String) = + IssueComments + .filter (_.byPrimaryKey(commentId)) + .map { t => + t.content ~ t.updatedDate + } + .update (content, currentDate) + + def deleteComment(commentId: Int) = + IssueComments filter (_.byPrimaryKey(commentId)) delete + + def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean) = + Issues + .filter (_.byPrimaryKey(owner, repository, issueId)) + .map { t => + t.closed ~ t.updatedDate + } + .update (closed, currentDate) + + /** + * Search issues by keyword. + * + * @param owner the repository owner + * @param repository the repository name + * @param query the keywords separated by whitespace. + * @return issues with comment count and matched content of issue or comment + */ + def searchIssuesByKeyword(owner: String, repository: String, query: String): List[(Issue, Int, String)] = { + import scala.slick.driver.H2Driver.likeEncode + val keywords = splitWords(query.toLowerCase) + + // Search Issue + val issues = Issues + .innerJoin(IssueOutline).on { case (t1, t2) => + t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) + } + .filter { case (t1, t2) => + keywords.map { keyword => + (t1.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) || + (t1.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) + } .reduceLeft(_ && _) + } + .map { case (t1, t2) => + (t1, 0, t1.content.?, t2.commentCount) + } + + // Search IssueComment + val comments = IssueComments + .innerJoin(Issues).on { case (t1, t2) => + t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) + } + .innerJoin(IssueOutline).on { case ((t1, t2), t3) => + t2.byIssue(t3.userName, t3.repositoryName, t3.issueId) + } + .filter { case ((t1, t2), t3) => + keywords.map { query => + t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^') + }.reduceLeft(_ && _) + } + .map { case ((t1, t2), t3) => + (t2, t1.commentId, t1.content.?, t3.commentCount) + } + + issues.union(comments).sortBy { case (issue, commentId, _, _) => + issue.issueId ~ commentId + }.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) => + issue1.issueId == issue2.issueId + }.map { _.head match { + case (issue, _, content, commentCount) => (issue, commentCount, content.getOrElse("")) + } + }.toList + } + + def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String) = { + extractCloseId(message).foreach { issueId => + for(issue <- getIssue(owner, repository, issueId) if !issue.closed){ + createComment(owner, repository, userName, issue.issueId, "Close", "close") + updateClosed(owner, repository, issue.issueId, true) + } + } + } +} + +object IssuesService { + import javax.servlet.http.HttpServletRequest + + val IssueLimit = 30 + + case class IssueSearchCondition( + labels: Set[String] = Set.empty, + milestoneId: Option[Option[Int]] = None, + repo: Option[String] = None, + state: String = "open", + sort: String = "created", + direction: String = "desc"){ + + def toURL: String = + "?" + List( + if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))), + milestoneId.map { id => "milestone=" + (id match { + case Some(x) => x.toString + case None => "none" + })}, + repo.map("for=" + urlEncode(_)), + Some("state=" + urlEncode(state)), + Some("sort=" + urlEncode(sort)), + Some("direction=" + urlEncode(direction))).flatten.mkString("&") + + } + + object IssueSearchCondition { + + private def param(request: HttpServletRequest, name: String, allow: Seq[String] = Nil): Option[String] = { + val value = request.getParameter(name) + if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value) + } + + def apply(request: HttpServletRequest): IssueSearchCondition = + IssueSearchCondition( + param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty), + param(request, "milestone").map{ + case "none" => None + case x => x.toIntOpt + }, + param(request, "for"), + param(request, "state", Seq("open", "closed")).getOrElse("open"), + param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"), + param(request, "direction", Seq("asc", "desc")).getOrElse("desc")) + + def page(request: HttpServletRequest) = try { + val i = param(request, "page").getOrElse("1").toInt + if(i <= 0) 1 else i + } catch { + case e: NumberFormatException => 1 + } + } + +} diff --git a/src/main/scala/service/SystemSettingsService.scala b/src/main/scala/service/SystemSettingsService.scala index fcceecb..42f3a75 100644 --- a/src/main/scala/service/SystemSettingsService.scala +++ b/src/main/scala/service/SystemSettingsService.scala @@ -1,190 +1,190 @@ -package service - -import util.Directory._ -import util.ControlUtil._ -import SystemSettingsService._ -import javax.servlet.http.HttpServletRequest - -trait SystemSettingsService { - - def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings().baseUrl(request) - - def saveSystemSettings(settings: SystemSettings): Unit = { - defining(new java.util.Properties()){ props => - settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", ""))) - props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString) - props.setProperty(Gravatar, settings.gravatar.toString) - props.setProperty(Notification, settings.notification.toString) - props.setProperty(Ssh, settings.ssh.toString) - settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString)) - if(settings.notification) { - settings.smtp.foreach { smtp => - props.setProperty(SmtpHost, smtp.host) - smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString)) - smtp.user.foreach(props.setProperty(SmtpUser, _)) - smtp.password.foreach(props.setProperty(SmtpPassword, _)) - smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString)) - smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _)) - smtp.fromName.foreach(props.setProperty(SmtpFromName, _)) - } - } - props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString) - if(settings.ldapAuthentication){ - settings.ldap.map { ldap => - props.setProperty(LdapHost, ldap.host) - ldap.port.foreach(x => props.setProperty(LdapPort, x.toString)) - ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x)) - ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x)) - props.setProperty(LdapBaseDN, ldap.baseDN) - props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute) - ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x)) - props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute) - ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString)) - ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x)) - } - } - using(new java.io.FileOutputStream(GitBucketConf)){ out => - props.store(out, null) - } - } - } - - - def loadSystemSettings(): SystemSettings = { - defining(new java.util.Properties()){ props => - if(GitBucketConf.exists){ - using(new java.io.FileInputStream(GitBucketConf)){ in => - props.load(in) - } - } - SystemSettings( - getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")), - getValue(props, AllowAccountRegistration, false), - getValue(props, Gravatar, true), - getValue(props, Notification, false), - getValue(props, Ssh, false), - getOptionValue(props, SshPort, Some(DefaultSshPort)), - if(getValue(props, Notification, false)){ - Some(Smtp( - getValue(props, SmtpHost, ""), - getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)), - getOptionValue(props, SmtpUser, None), - getOptionValue(props, SmtpPassword, None), - getOptionValue[Boolean](props, SmtpSsl, None), - getOptionValue(props, SmtpFromAddress, None), - getOptionValue(props, SmtpFromName, None))) - } else { - None - }, - getValue(props, LdapAuthentication, false), - if(getValue(props, LdapAuthentication, false)){ - Some(Ldap( - getValue(props, LdapHost, ""), - getOptionValue(props, LdapPort, Some(DefaultLdapPort)), - getOptionValue(props, LdapBindDN, None), - getOptionValue(props, LdapBindPassword, None), - getValue(props, LdapBaseDN, ""), - getValue(props, LdapUserNameAttribute, ""), - getOptionValue(props, LdapFullNameAttribute, None), - getValue(props, LdapMailAddressAttribute, ""), - getOptionValue[Boolean](props, LdapTls, None), - getOptionValue(props, LdapKeystore, None))) - } else { - None - } - ) - } - } - -} - -object SystemSettingsService { - import scala.reflect.ClassTag - - case class SystemSettings( - baseUrl: Option[String], - allowAccountRegistration: Boolean, - gravatar: Boolean, - notification: Boolean, - ssh: Boolean, - sshPort: Option[Int], - smtp: Option[Smtp], - ldapAuthentication: Boolean, - ldap: Option[Ldap]){ - def baseUrl(request: HttpServletRequest): String = baseUrl.getOrElse { - defining(request.getRequestURL.toString){ url => - url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length)) - } - }.stripSuffix("/") - } - - case class Ldap( - host: String, - port: Option[Int], - bindDN: Option[String], - bindPassword: Option[String], - baseDN: String, - userNameAttribute: String, - fullNameAttribute: Option[String], - mailAttribute: String, - tls: Option[Boolean], - keystore: Option[String]) - - case class Smtp( - host: String, - port: Option[Int], - user: Option[String], - password: Option[String], - ssl: Option[Boolean], - fromAddress: Option[String], - fromName: Option[String]) - - val DefaultSshPort = 29418 - val DefaultSmtpPort = 25 - val DefaultLdapPort = 389 - - private val BaseURL = "base_url" - private val AllowAccountRegistration = "allow_account_registration" - private val Gravatar = "gravatar" - private val Notification = "notification" - private val Ssh = "ssh" - private val SshPort = "ssh.port" - private val SmtpHost = "smtp.host" - private val SmtpPort = "smtp.port" - private val SmtpUser = "smtp.user" - private val SmtpPassword = "smtp.password" - private val SmtpSsl = "smtp.ssl" - private val SmtpFromAddress = "smtp.from_address" - private val SmtpFromName = "smtp.from_name" - private val LdapAuthentication = "ldap_authentication" - private val LdapHost = "ldap.host" - private val LdapPort = "ldap.port" - private val LdapBindDN = "ldap.bindDN" - private val LdapBindPassword = "ldap.bind_password" - private val LdapBaseDN = "ldap.baseDN" - private val LdapUserNameAttribute = "ldap.username_attribute" - private val LdapFullNameAttribute = "ldap.fullname_attribute" - private val LdapMailAddressAttribute = "ldap.mail_attribute" - private val LdapTls = "ldap.tls" - private val LdapKeystore = "ldap.keystore" - - private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = - defining(props.getProperty(key)){ value => - if(value == null || value.isEmpty) default - else convertType(value).asInstanceOf[A] - } - - private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] = - defining(props.getProperty(key)){ value => - if(value == null || value.isEmpty) default - else Some(convertType(value)).asInstanceOf[Option[A]] - } - - private def convertType[A: ClassTag](value: String) = - defining(implicitly[ClassTag[A]].runtimeClass){ c => - if(c == classOf[Boolean]) value.toBoolean - else if(c == classOf[Int]) value.toInt - else value - } - -} +package service + +import util.Directory._ +import util.ControlUtil._ +import SystemSettingsService._ +import javax.servlet.http.HttpServletRequest + +trait SystemSettingsService { + + def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings().baseUrl(request) + + def saveSystemSettings(settings: SystemSettings): Unit = { + defining(new java.util.Properties()){ props => + settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", ""))) + props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString) + props.setProperty(Gravatar, settings.gravatar.toString) + props.setProperty(Notification, settings.notification.toString) + props.setProperty(Ssh, settings.ssh.toString) + settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString)) + if(settings.notification) { + settings.smtp.foreach { smtp => + props.setProperty(SmtpHost, smtp.host) + smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString)) + smtp.user.foreach(props.setProperty(SmtpUser, _)) + smtp.password.foreach(props.setProperty(SmtpPassword, _)) + smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString)) + smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _)) + smtp.fromName.foreach(props.setProperty(SmtpFromName, _)) + } + } + props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString) + if(settings.ldapAuthentication){ + settings.ldap.map { ldap => + props.setProperty(LdapHost, ldap.host) + ldap.port.foreach(x => props.setProperty(LdapPort, x.toString)) + ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x)) + ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x)) + props.setProperty(LdapBaseDN, ldap.baseDN) + props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute) + ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x)) + props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute) + ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString)) + ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x)) + } + } + using(new java.io.FileOutputStream(GitBucketConf)){ out => + props.store(out, null) + } + } + } + + + def loadSystemSettings(): SystemSettings = { + defining(new java.util.Properties()){ props => + if(GitBucketConf.exists){ + using(new java.io.FileInputStream(GitBucketConf)){ in => + props.load(in) + } + } + SystemSettings( + getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")), + getValue(props, AllowAccountRegistration, false), + getValue(props, Gravatar, true), + getValue(props, Notification, false), + getValue(props, Ssh, false), + getOptionValue(props, SshPort, Some(DefaultSshPort)), + if(getValue(props, Notification, false)){ + Some(Smtp( + getValue(props, SmtpHost, ""), + getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)), + getOptionValue(props, SmtpUser, None), + getOptionValue(props, SmtpPassword, None), + getOptionValue[Boolean](props, SmtpSsl, None), + getOptionValue(props, SmtpFromAddress, None), + getOptionValue(props, SmtpFromName, None))) + } else { + None + }, + getValue(props, LdapAuthentication, false), + if(getValue(props, LdapAuthentication, false)){ + Some(Ldap( + getValue(props, LdapHost, ""), + getOptionValue(props, LdapPort, Some(DefaultLdapPort)), + getOptionValue(props, LdapBindDN, None), + getOptionValue(props, LdapBindPassword, None), + getValue(props, LdapBaseDN, ""), + getValue(props, LdapUserNameAttribute, ""), + getOptionValue(props, LdapFullNameAttribute, None), + getValue(props, LdapMailAddressAttribute, ""), + getOptionValue[Boolean](props, LdapTls, None), + getOptionValue(props, LdapKeystore, None))) + } else { + None + } + ) + } + } + +} + +object SystemSettingsService { + import scala.reflect.ClassTag + + case class SystemSettings( + baseUrl: Option[String], + allowAccountRegistration: Boolean, + gravatar: Boolean, + notification: Boolean, + ssh: Boolean, + sshPort: Option[Int], + smtp: Option[Smtp], + ldapAuthentication: Boolean, + ldap: Option[Ldap]){ + def baseUrl(request: HttpServletRequest): String = baseUrl.getOrElse { + defining(request.getRequestURL.toString){ url => + url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length)) + } + }.stripSuffix("/") + } + + case class Ldap( + host: String, + port: Option[Int], + bindDN: Option[String], + bindPassword: Option[String], + baseDN: String, + userNameAttribute: String, + fullNameAttribute: Option[String], + mailAttribute: String, + tls: Option[Boolean], + keystore: Option[String]) + + case class Smtp( + host: String, + port: Option[Int], + user: Option[String], + password: Option[String], + ssl: Option[Boolean], + fromAddress: Option[String], + fromName: Option[String]) + + val DefaultSshPort = 29418 + val DefaultSmtpPort = 25 + val DefaultLdapPort = 389 + + private val BaseURL = "base_url" + private val AllowAccountRegistration = "allow_account_registration" + private val Gravatar = "gravatar" + private val Notification = "notification" + private val Ssh = "ssh" + private val SshPort = "ssh.port" + private val SmtpHost = "smtp.host" + private val SmtpPort = "smtp.port" + private val SmtpUser = "smtp.user" + private val SmtpPassword = "smtp.password" + private val SmtpSsl = "smtp.ssl" + private val SmtpFromAddress = "smtp.from_address" + private val SmtpFromName = "smtp.from_name" + private val LdapAuthentication = "ldap_authentication" + private val LdapHost = "ldap.host" + private val LdapPort = "ldap.port" + private val LdapBindDN = "ldap.bindDN" + private val LdapBindPassword = "ldap.bind_password" + private val LdapBaseDN = "ldap.baseDN" + private val LdapUserNameAttribute = "ldap.username_attribute" + private val LdapFullNameAttribute = "ldap.fullname_attribute" + private val LdapMailAddressAttribute = "ldap.mail_attribute" + private val LdapTls = "ldap.tls" + private val LdapKeystore = "ldap.keystore" + + private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = + defining(props.getProperty(key)){ value => + if(value == null || value.isEmpty) default + else convertType(value).asInstanceOf[A] + } + + private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] = + defining(props.getProperty(key)){ value => + if(value == null || value.isEmpty) default + else Some(convertType(value)).asInstanceOf[Option[A]] + } + + private def convertType[A: ClassTag](value: String) = + defining(implicitly[ClassTag[A]].runtimeClass){ c => + if(c == classOf[Boolean]) value.toBoolean + else if(c == classOf[Int]) value.toInt + else value + } + +} diff --git a/src/main/scala/service/WikiService.scala b/src/main/scala/service/WikiService.scala index 7f5f908..4016a6e 100644 --- a/src/main/scala/service/WikiService.scala +++ b/src/main/scala/service/WikiService.scala @@ -1,282 +1,282 @@ -package service - -import java.util.Date -import org.eclipse.jgit.api.Git -import org.apache.commons.io.FileUtils -import util._ -import _root_.util.ControlUtil._ -import org.eclipse.jgit.treewalk.{TreeWalk, CanonicalTreeParser} -import org.eclipse.jgit.lib._ -import org.eclipse.jgit.dircache.{DirCache, DirCacheEntry} -import org.eclipse.jgit.revwalk.RevWalk -import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter} -import java.io.ByteArrayInputStream -import org.eclipse.jgit.patch._ -import org.eclipse.jgit.api.errors.PatchFormatException -import scala.collection.JavaConverters._ -import scala.Some -import service.RepositoryService.RepositoryInfo - - -object WikiService { - - /** - * The model for wiki page. - * - * @param name the page name - * @param content the page content - * @param committer the last committer - * @param time the last modified time - * @param id the latest commit id - */ - case class WikiPageInfo(name: String, content: String, committer: String, time: Date, id: String) - - /** - * The model for wiki page history. - * - * @param name the page name - * @param committer the committer the committer - * @param message the commit message - * @param date the commit date - */ - case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date) - - def httpUrl(repository: RepositoryInfo) = repository.httpUrl.replaceFirst("\\.git\\Z", ".wiki.git") - - def sshUrl(repository: RepositoryInfo, settings: SystemSettingsService.SystemSettings, userName: String) = - repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), userName).replaceFirst("\\.git\\Z", ".wiki.git") -} - -trait WikiService { - import WikiService._ - - def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit = - LockUtil.lock(s"${owner}/${repository}/wiki"){ - defining(Directory.getWikiRepositoryDir(owner, repository)){ dir => - if(!dir.exists){ - JGitUtil.initRepository(dir) - saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None) - } - } - } - - /** - * Returns the wiki page. - */ - def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = { - using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => - if(!JGitUtil.isEmpty(git)){ - JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file => - WikiPageInfo(file.name, StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes), - file.committer, file.time, file.commitId) - } - } else None - } - } - - /** - * Returns the content of the specified file. - */ - def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] = - using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => - if(!JGitUtil.isEmpty(git)){ - val index = path.lastIndexOf('/') - val parentPath = if(index < 0) "." else path.substring(0, index) - val fileName = if(index < 0) path else path.substring(index + 1) - - JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file => - git.getRepository.open(file.id).getBytes - } - } else None - } - - /** - * Returns the list of wiki page names. - */ - def getWikiPageList(owner: String, repository: String): List[String] = { - using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => - JGitUtil.getFileList(git, "master", ".") - .filter(_.name.endsWith(".md")) - .map(_.name.stripSuffix(".md")) - .sortBy(x => x) - } - } - - /** - * Reverts specified changes. - */ - def revertWikiPage(owner: String, repository: String, from: String, to: String, - committer: model.Account, pageName: Option[String]): Boolean = { - - case class RevertInfo(operation: String, filePath: String, source: String) - - try { - LockUtil.lock(s"${owner}/${repository}/wiki"){ - using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => - - val reader = git.getRepository.newObjectReader - val oldTreeIter = new CanonicalTreeParser - oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}")) - - val newTreeIter = new CanonicalTreeParser - newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}")) - - val diffs = git.diff.setNewTree(oldTreeIter).setOldTree(newTreeIter).call.asScala.filter { diff => - pageName match { - case Some(x) => diff.getNewPath == x + ".md" - case None => true - } - } - - val patch = using(new java.io.ByteArrayOutputStream()){ out => - val formatter = new DiffFormatter(out) - formatter.setRepository(git.getRepository) - formatter.format(diffs.asJava) - new String(out.toByteArray, "UTF-8") - } - - val p = new Patch() - p.parse(new ByteArrayInputStream(patch.getBytes("UTF-8"))) - if(!p.getErrors.isEmpty){ - throw new PatchFormatException(p.getErrors()) - } - val revertInfo = (p.getFiles.asScala.map { fh => - fh.getChangeType match { - case DiffEntry.ChangeType.MODIFY => { - val source = getWikiPage(owner, repository, fh.getNewPath.stripSuffix(".md")).map(_.content).getOrElse("") - val applied = PatchUtil.apply(source, patch, fh) - if(applied != null){ - Seq(RevertInfo("ADD", fh.getNewPath, applied)) - } else Nil - } - case DiffEntry.ChangeType.ADD => { - val applied = PatchUtil.apply("", patch, fh) - if(applied != null){ - Seq(RevertInfo("ADD", fh.getNewPath, applied)) - } else Nil - } - case DiffEntry.ChangeType.DELETE => { - Seq(RevertInfo("DELETE", fh.getNewPath, "")) - } - case DiffEntry.ChangeType.RENAME => { - val applied = PatchUtil.apply("", patch, fh) - if(applied != null){ - Seq(RevertInfo("DELETE", fh.getOldPath, ""), RevertInfo("ADD", fh.getNewPath, applied)) - } else { - Seq(RevertInfo("DELETE", fh.getOldPath, "")) - } - } - case _ => Nil - } - }).flatten - - if(revertInfo.nonEmpty){ - val builder = DirCache.newInCore.builder() - val inserter = git.getRepository.newObjectInserter() - val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") - - JGitUtil.processTree(git, headId){ (path, tree) => - if(revertInfo.find(x => x.filePath == path).isEmpty){ - builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) - } - } - - revertInfo.filter(_.operation == "ADD").foreach { x => - builder.add(JGitUtil.createDirCacheEntry(x.filePath, FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, x.source.getBytes("UTF-8")))) - } - builder.finish() - - JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress, - pageName match { - case Some(x) => s"Revert ${from} ... ${to} on ${x}" - case None => s"Revert ${from} ... ${to}" - }) - } - } - } - true - } catch { - case e: Exception => { - e.printStackTrace() - false - } - } - } - - /** - * Save the wiki page. - */ - def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String, - content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = { - LockUtil.lock(s"${owner}/${repository}/wiki"){ - using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => - val builder = DirCache.newInCore.builder() - val inserter = git.getRepository.newObjectInserter() - val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") - var created = true - var updated = false - var removed = false - - if(headId != null){ - JGitUtil.processTree(git, headId){ (path, tree) => - if(path == currentPageName + ".md" && currentPageName != newPageName){ - removed = true - } else if(path != newPageName + ".md"){ - builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) - } else { - created = false - updated = JGitUtil.getContentFromId(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false) - } - } - } - - if(created || updated || removed){ - builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")))) - builder.finish() - val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress, - if(message.trim.length == 0) { - if(removed){ - s"Rename ${currentPageName} to ${newPageName}" - } else if(created){ - s"Created ${newPageName}" - } else { - s"Updated ${newPageName}" - } - } else { - message - }) - - Some(newHeadId.getName) - } else None - } - } - } - - /** - * Delete the wiki page. - */ - def deleteWikiPage(owner: String, repository: String, pageName: String, - committer: String, mailAddress: String, message: String): Unit = { - LockUtil.lock(s"${owner}/${repository}/wiki"){ - using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => - val builder = DirCache.newInCore.builder() - val inserter = git.getRepository.newObjectInserter() - val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") - var removed = false - - JGitUtil.processTree(git, headId){ (path, tree) => - if(path != pageName + ".md"){ - builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) - } else { - removed = true - } - } - if(removed){ - builder.finish() - JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message) - } - } - } - } - -} +package service + +import java.util.Date +import org.eclipse.jgit.api.Git +import org.apache.commons.io.FileUtils +import util._ +import _root_.util.ControlUtil._ +import org.eclipse.jgit.treewalk.{TreeWalk, CanonicalTreeParser} +import org.eclipse.jgit.lib._ +import org.eclipse.jgit.dircache.{DirCache, DirCacheEntry} +import org.eclipse.jgit.revwalk.RevWalk +import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter} +import java.io.ByteArrayInputStream +import org.eclipse.jgit.patch._ +import org.eclipse.jgit.api.errors.PatchFormatException +import scala.collection.JavaConverters._ +import scala.Some +import service.RepositoryService.RepositoryInfo + + +object WikiService { + + /** + * The model for wiki page. + * + * @param name the page name + * @param content the page content + * @param committer the last committer + * @param time the last modified time + * @param id the latest commit id + */ + case class WikiPageInfo(name: String, content: String, committer: String, time: Date, id: String) + + /** + * The model for wiki page history. + * + * @param name the page name + * @param committer the committer the committer + * @param message the commit message + * @param date the commit date + */ + case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date) + + def httpUrl(repository: RepositoryInfo) = repository.httpUrl.replaceFirst("\\.git\\Z", ".wiki.git") + + def sshUrl(repository: RepositoryInfo, settings: SystemSettingsService.SystemSettings, userName: String) = + repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), userName).replaceFirst("\\.git\\Z", ".wiki.git") +} + +trait WikiService { + import WikiService._ + + def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit = + LockUtil.lock(s"${owner}/${repository}/wiki"){ + defining(Directory.getWikiRepositoryDir(owner, repository)){ dir => + if(!dir.exists){ + JGitUtil.initRepository(dir) + saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None) + } + } + } + + /** + * Returns the wiki page. + */ + def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = { + using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => + if(!JGitUtil.isEmpty(git)){ + JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file => + WikiPageInfo(file.name, StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes), + file.committer, file.time, file.commitId) + } + } else None + } + } + + /** + * Returns the content of the specified file. + */ + def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] = + using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => + if(!JGitUtil.isEmpty(git)){ + val index = path.lastIndexOf('/') + val parentPath = if(index < 0) "." else path.substring(0, index) + val fileName = if(index < 0) path else path.substring(index + 1) + + JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file => + git.getRepository.open(file.id).getBytes + } + } else None + } + + /** + * Returns the list of wiki page names. + */ + def getWikiPageList(owner: String, repository: String): List[String] = { + using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => + JGitUtil.getFileList(git, "master", ".") + .filter(_.name.endsWith(".md")) + .map(_.name.stripSuffix(".md")) + .sortBy(x => x) + } + } + + /** + * Reverts specified changes. + */ + def revertWikiPage(owner: String, repository: String, from: String, to: String, + committer: model.Account, pageName: Option[String]): Boolean = { + + case class RevertInfo(operation: String, filePath: String, source: String) + + try { + LockUtil.lock(s"${owner}/${repository}/wiki"){ + using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => + + val reader = git.getRepository.newObjectReader + val oldTreeIter = new CanonicalTreeParser + oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}")) + + val newTreeIter = new CanonicalTreeParser + newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}")) + + val diffs = git.diff.setNewTree(oldTreeIter).setOldTree(newTreeIter).call.asScala.filter { diff => + pageName match { + case Some(x) => diff.getNewPath == x + ".md" + case None => true + } + } + + val patch = using(new java.io.ByteArrayOutputStream()){ out => + val formatter = new DiffFormatter(out) + formatter.setRepository(git.getRepository) + formatter.format(diffs.asJava) + new String(out.toByteArray, "UTF-8") + } + + val p = new Patch() + p.parse(new ByteArrayInputStream(patch.getBytes("UTF-8"))) + if(!p.getErrors.isEmpty){ + throw new PatchFormatException(p.getErrors()) + } + val revertInfo = (p.getFiles.asScala.map { fh => + fh.getChangeType match { + case DiffEntry.ChangeType.MODIFY => { + val source = getWikiPage(owner, repository, fh.getNewPath.stripSuffix(".md")).map(_.content).getOrElse("") + val applied = PatchUtil.apply(source, patch, fh) + if(applied != null){ + Seq(RevertInfo("ADD", fh.getNewPath, applied)) + } else Nil + } + case DiffEntry.ChangeType.ADD => { + val applied = PatchUtil.apply("", patch, fh) + if(applied != null){ + Seq(RevertInfo("ADD", fh.getNewPath, applied)) + } else Nil + } + case DiffEntry.ChangeType.DELETE => { + Seq(RevertInfo("DELETE", fh.getNewPath, "")) + } + case DiffEntry.ChangeType.RENAME => { + val applied = PatchUtil.apply("", patch, fh) + if(applied != null){ + Seq(RevertInfo("DELETE", fh.getOldPath, ""), RevertInfo("ADD", fh.getNewPath, applied)) + } else { + Seq(RevertInfo("DELETE", fh.getOldPath, "")) + } + } + case _ => Nil + } + }).flatten + + if(revertInfo.nonEmpty){ + val builder = DirCache.newInCore.builder() + val inserter = git.getRepository.newObjectInserter() + val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") + + JGitUtil.processTree(git, headId){ (path, tree) => + if(revertInfo.find(x => x.filePath == path).isEmpty){ + builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) + } + } + + revertInfo.filter(_.operation == "ADD").foreach { x => + builder.add(JGitUtil.createDirCacheEntry(x.filePath, FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, x.source.getBytes("UTF-8")))) + } + builder.finish() + + JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress, + pageName match { + case Some(x) => s"Revert ${from} ... ${to} on ${x}" + case None => s"Revert ${from} ... ${to}" + }) + } + } + } + true + } catch { + case e: Exception => { + e.printStackTrace() + false + } + } + } + + /** + * Save the wiki page. + */ + def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String, + content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = { + LockUtil.lock(s"${owner}/${repository}/wiki"){ + using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => + val builder = DirCache.newInCore.builder() + val inserter = git.getRepository.newObjectInserter() + val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") + var created = true + var updated = false + var removed = false + + if(headId != null){ + JGitUtil.processTree(git, headId){ (path, tree) => + if(path == currentPageName + ".md" && currentPageName != newPageName){ + removed = true + } else if(path != newPageName + ".md"){ + builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) + } else { + created = false + updated = JGitUtil.getContentFromId(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false) + } + } + } + + if(created || updated || removed){ + builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")))) + builder.finish() + val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress, + if(message.trim.length == 0) { + if(removed){ + s"Rename ${currentPageName} to ${newPageName}" + } else if(created){ + s"Created ${newPageName}" + } else { + s"Updated ${newPageName}" + } + } else { + message + }) + + Some(newHeadId.getName) + } else None + } + } + } + + /** + * Delete the wiki page. + */ + def deleteWikiPage(owner: String, repository: String, pageName: String, + committer: String, mailAddress: String, message: String): Unit = { + LockUtil.lock(s"${owner}/${repository}/wiki"){ + using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => + val builder = DirCache.newInCore.builder() + val inserter = git.getRepository.newObjectInserter() + val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") + var removed = false + + JGitUtil.processTree(git, headId){ (path, tree) => + if(path != pageName + ".md"){ + builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) + } else { + removed = true + } + } + if(removed){ + builder.finish() + JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message) + } + } + } + } + +} diff --git a/src/main/scala/servlet/AutoUpdateListener.scala b/src/main/scala/servlet/AutoUpdateListener.scala index 801ff68..f47c489 100644 --- a/src/main/scala/servlet/AutoUpdateListener.scala +++ b/src/main/scala/servlet/AutoUpdateListener.scala @@ -1,193 +1,193 @@ -package servlet - -import java.io.File -import java.sql.{DriverManager, Connection} -import org.apache.commons.io.FileUtils -import javax.servlet.{ServletContext, ServletContextListener, ServletContextEvent} -import org.apache.commons.io.IOUtils -import org.slf4j.LoggerFactory -import util.Directory._ -import util.ControlUtil._ -import org.eclipse.jgit.api.Git -import util.Directory - -object AutoUpdate { - - /** - * Version of GitBucket - * - * @param majorVersion the major version - * @param minorVersion the minor version - */ - case class Version(majorVersion: Int, minorVersion: Int){ - - 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. - */ - def update(conn: Connection): Unit = { - val sqlPath = s"update/${majorVersion}_${minorVersion}.sql" - - using(Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)){ in => - if(in != null){ - val sql = IOUtils.toString(in, "UTF-8") - using(conn.createStatement()){ stmt => - logger.debug(sqlPath + "=" + sql) - stmt.executeUpdate(sql) - } - } - } - } - - /** - * MAJOR.MINOR - */ - val versionString = s"${majorVersion}.${minorVersion}" - } - - /** - * The history of versions. A head of this sequence is the current BitBucket version. - */ - val versions = Seq( - new Version(2, 0){ - override def update(conn: Connection): Unit = { - import eu.medsea.mimeutil.{MimeUtil2, MimeType} - - 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 => - while(rs.next){ - defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir => - if(dir.exists && dir.isDirectory){ - dir.listFiles.foreach { file => - if(file.getName.indexOf('.') < 0){ - val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString - if(mimeType.startsWith("image/")){ - file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1))) - } - } - } - } - } - } - } - } - }, - Version(1, 13), - Version(1, 12), - Version(1, 11), - Version(1, 10), - Version(1, 9), - Version(1, 8), - Version(1, 7), - Version(1, 6), - Version(1, 5), - Version(1, 4), - new Version(1, 3){ - override def update(conn: Connection): Unit = { - super.update(conn) - // Fix wiki repository configuration - using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs => - while(rs.next){ - using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git => - defining(git.getRepository.getConfig){ config => - if(!config.getBoolean("http", "receivepack", false)){ - config.setBoolean("http", null, "receivepack", true) - config.save - } - } - } - } - } - } - }, - Version(1, 2), - Version(1, 1), - Version(1, 0), - Version(0, 0) - ) - - /** - * The head version of BitBucket. - */ - val headVersion = versions.head - - /** - * The version file (GITBUCKET_HOME/version). - */ - lazy val versionFile = new File(GitBucketHome, "version") - - /** - * Returns the current version from the version file. - */ - def getCurrentVersion(): Version = { - if(versionFile.exists){ - FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match { - case Array(majorVersion, minorVersion) => { - versions.find { v => - v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt - }.getOrElse(Version(0, 0)) - } - case _ => Version(0, 0) - } - } else Version(0, 0) - } - -} - -/** - * Update database schema automatically in the context initializing. - */ -class AutoUpdateListener extends ServletContextListener { - import AutoUpdate._ - private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener]) - - override def contextInitialized(event: ServletContextEvent): Unit = { - val datadir = event.getServletContext.getInitParameter("gitbucket.home") - if(datadir != null){ - System.setProperty("gitbucket.home", datadir) - } - org.h2.Driver.load() - event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true") - - logger.debug("Start schema update") - defining(getConnection(event.getServletContext)){ conn => - try { - defining(getCurrentVersion()){ currentVersion => - if(currentVersion == headVersion){ - logger.debug("No update") - } else if(!versions.contains(currentVersion)){ - logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.") - } else { - versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn)) - FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8") - conn.commit() - logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}") - } - } - } catch { - case ex: Throwable => { - logger.error("Failed to schema update", ex) - ex.printStackTrace() - conn.rollback() - } - } - } - logger.debug("End schema update") - } - - def contextDestroyed(sce: ServletContextEvent): Unit = { - // Nothing to do. - } - - private def getConnection(servletContext: ServletContext): Connection = - DriverManager.getConnection( - servletContext.getInitParameter("db.url"), - servletContext.getInitParameter("db.user"), - servletContext.getInitParameter("db.password")) - -} +package servlet + +import java.io.File +import java.sql.{DriverManager, Connection} +import org.apache.commons.io.FileUtils +import javax.servlet.{ServletContext, ServletContextListener, ServletContextEvent} +import org.apache.commons.io.IOUtils +import org.slf4j.LoggerFactory +import util.Directory._ +import util.ControlUtil._ +import org.eclipse.jgit.api.Git +import util.Directory + +object AutoUpdate { + + /** + * Version of GitBucket + * + * @param majorVersion the major version + * @param minorVersion the minor version + */ + case class Version(majorVersion: Int, minorVersion: Int){ + + 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. + */ + def update(conn: Connection): Unit = { + val sqlPath = s"update/${majorVersion}_${minorVersion}.sql" + + using(Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)){ in => + if(in != null){ + val sql = IOUtils.toString(in, "UTF-8") + using(conn.createStatement()){ stmt => + logger.debug(sqlPath + "=" + sql) + stmt.executeUpdate(sql) + } + } + } + } + + /** + * MAJOR.MINOR + */ + val versionString = s"${majorVersion}.${minorVersion}" + } + + /** + * The history of versions. A head of this sequence is the current BitBucket version. + */ + val versions = Seq( + new Version(2, 0){ + override def update(conn: Connection): Unit = { + import eu.medsea.mimeutil.{MimeUtil2, MimeType} + + 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 => + while(rs.next){ + defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir => + if(dir.exists && dir.isDirectory){ + dir.listFiles.foreach { file => + if(file.getName.indexOf('.') < 0){ + val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString + if(mimeType.startsWith("image/")){ + file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1))) + } + } + } + } + } + } + } + } + }, + Version(1, 13), + Version(1, 12), + Version(1, 11), + Version(1, 10), + Version(1, 9), + Version(1, 8), + Version(1, 7), + Version(1, 6), + Version(1, 5), + Version(1, 4), + new Version(1, 3){ + override def update(conn: Connection): Unit = { + super.update(conn) + // Fix wiki repository configuration + using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs => + while(rs.next){ + using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git => + defining(git.getRepository.getConfig){ config => + if(!config.getBoolean("http", "receivepack", false)){ + config.setBoolean("http", null, "receivepack", true) + config.save + } + } + } + } + } + } + }, + Version(1, 2), + Version(1, 1), + Version(1, 0), + Version(0, 0) + ) + + /** + * The head version of BitBucket. + */ + val headVersion = versions.head + + /** + * The version file (GITBUCKET_HOME/version). + */ + lazy val versionFile = new File(GitBucketHome, "version") + + /** + * Returns the current version from the version file. + */ + def getCurrentVersion(): Version = { + if(versionFile.exists){ + FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match { + case Array(majorVersion, minorVersion) => { + versions.find { v => + v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt + }.getOrElse(Version(0, 0)) + } + case _ => Version(0, 0) + } + } else Version(0, 0) + } + +} + +/** + * Update database schema automatically in the context initializing. + */ +class AutoUpdateListener extends ServletContextListener { + import AutoUpdate._ + private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener]) + + override def contextInitialized(event: ServletContextEvent): Unit = { + val datadir = event.getServletContext.getInitParameter("gitbucket.home") + if(datadir != null){ + System.setProperty("gitbucket.home", datadir) + } + org.h2.Driver.load() + event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true") + + logger.debug("Start schema update") + defining(getConnection(event.getServletContext)){ conn => + try { + defining(getCurrentVersion()){ currentVersion => + if(currentVersion == headVersion){ + logger.debug("No update") + } else if(!versions.contains(currentVersion)){ + logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.") + } else { + versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn)) + FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8") + conn.commit() + logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}") + } + } + } catch { + case ex: Throwable => { + logger.error("Failed to schema update", ex) + ex.printStackTrace() + conn.rollback() + } + } + } + logger.debug("End schema update") + } + + def contextDestroyed(sce: ServletContextEvent): Unit = { + // Nothing to do. + } + + private def getConnection(servletContext: ServletContext): Connection = + DriverManager.getConnection( + servletContext.getInitParameter("db.url"), + servletContext.getInitParameter("db.user"), + servletContext.getInitParameter("db.password")) + +} diff --git a/src/main/scala/servlet/TransactionFilter.scala b/src/main/scala/servlet/TransactionFilter.scala index 12363ad..f62e61a 100644 --- a/src/main/scala/servlet/TransactionFilter.scala +++ b/src/main/scala/servlet/TransactionFilter.scala @@ -1,38 +1,38 @@ -package servlet - -import javax.servlet._ -import org.slf4j.LoggerFactory -import javax.servlet.http.HttpServletRequest - -/** - * Controls the transaction with the open session in view pattern. - */ -class TransactionFilter extends Filter { - - private val logger = LoggerFactory.getLogger(classOf[TransactionFilter]) - - def init(config: FilterConfig) = {} - - def destroy(): Unit = {} - - def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = { - if(req.asInstanceOf[HttpServletRequest].getRequestURI().startsWith("/assets/")){ - // assets don't need transaction - chain.doFilter(req, res) - } else { - Database(req.getServletContext) withTransaction { - logger.debug("begin transaction") - chain.doFilter(req, res) - logger.debug("end transaction") - } - } - } - -} - -object Database { - def apply(context: ServletContext): scala.slick.session.Database = - scala.slick.session.Database.forURL(context.getInitParameter("db.url"), - context.getInitParameter("db.user"), - context.getInitParameter("db.password")) -} +package servlet + +import javax.servlet._ +import org.slf4j.LoggerFactory +import javax.servlet.http.HttpServletRequest + +/** + * Controls the transaction with the open session in view pattern. + */ +class TransactionFilter extends Filter { + + private val logger = LoggerFactory.getLogger(classOf[TransactionFilter]) + + def init(config: FilterConfig) = {} + + def destroy(): Unit = {} + + def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = { + if(req.asInstanceOf[HttpServletRequest].getRequestURI().startsWith("/assets/")){ + // assets don't need transaction + chain.doFilter(req, res) + } else { + Database(req.getServletContext) withTransaction { + logger.debug("begin transaction") + chain.doFilter(req, res) + logger.debug("end transaction") + } + } + } + +} + +object Database { + def apply(context: ServletContext): scala.slick.session.Database = + scala.slick.session.Database.forURL(context.getInitParameter("db.url"), + context.getInitParameter("db.user"), + context.getInitParameter("db.password")) +} diff --git a/src/main/scala/util/Notifier.scala b/src/main/scala/util/Notifier.scala index 21dfce6..304f440 100644 --- a/src/main/scala/util/Notifier.scala +++ b/src/main/scala/util/Notifier.scala @@ -1,116 +1,116 @@ -package util - -import scala.concurrent._ -import ExecutionContext.Implicits.global -import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail} -import org.slf4j.LoggerFactory - -import app.Context -import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService} -import servlet.Database -import SystemSettingsService.Smtp -import _root_.util.ControlUtil.defining - -trait Notifier extends RepositoryService with AccountService with IssuesService { - def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) - (msg: String => String)(implicit context: Context): Unit - - protected def recipients(issue: model.Issue)(notify: String => Unit)(implicit context: Context) = - ( - // individual repository's owner - issue.userName :: - // collaborators - getCollaborators(issue.userName, issue.repositoryName) ::: - // participants - issue.openedUserName :: - getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName) - ) - .distinct - .withFilter ( _ != context.loginAccount.get.userName ) // the operation in person is excluded - .foreach ( getAccountByUserName(_) filterNot (_.isGroupAccount) foreach (x => notify(x.mailAddress)) ) - -} - -object Notifier { - // TODO We want to be able to switch to mock. - def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match { - case settings if settings.notification => new Mailer(settings.smtp.get) - case _ => new MockMailer - } - - def msgIssue(url: String) = (content: String) => s""" - |${content}
- |--
- |View it on GitBucket - """.stripMargin - - def msgPullRequest(url: String) = (content: String) => s""" - |${content}
- |View, comment on, or merge it at:
- |${url} - """.stripMargin - - def msgComment(url: String) = (content: String) => s""" - |${content}
- |--
- |View it on GitBucket - """.stripMargin - - def msgStatus(url: String) = (content: String) => s""" - |${content} #${url split('/') last} - """.stripMargin -} - -class Mailer(private val smtp: Smtp) extends Notifier { - private val logger = LoggerFactory.getLogger(classOf[Mailer]) - - def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) - (msg: String => String)(implicit context: Context) = { - val database = Database(context.request.getServletContext) - - val f = future { - // TODO Can we use the Database Session in other than Transaction Filter? - database withSession { - getIssue(r.owner, r.name, issueId.toString) foreach { issue => - defining( - s"[${r.name}] ${issue.title} (#${issueId})" -> - msg(view.Markdown.toHtml(content, r, false, true))) { case (subject, msg) => - recipients(issue) { to => - val email = new HtmlEmail - email.setHostName(smtp.host) - email.setSmtpPort(smtp.port.get) - smtp.user.foreach { user => - email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse(""))) - } - smtp.ssl.foreach { ssl => - email.setSSLOnConnect(ssl) - } - smtp.fromAddress - .map (_ -> smtp.fromName.orNull) - .orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName)) - .foreach { case (address, name) => - email.setFrom(address, name) - } - email.setCharset("UTF-8") - email.setSubject(subject) - email.setHtmlMsg(msg) - - email.addTo(to).send - } - } - } - } - "Notifications Successful." - } - f onSuccess { - case s => logger.debug(s) - } - f onFailure { - case t => logger.error("Notifications Failed.", t) - } - } -} -class MockMailer extends Notifier { - def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) - (msg: String => String)(implicit context: Context): Unit = {} +package util + +import scala.concurrent._ +import ExecutionContext.Implicits.global +import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail} +import org.slf4j.LoggerFactory + +import app.Context +import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService} +import servlet.Database +import SystemSettingsService.Smtp +import _root_.util.ControlUtil.defining + +trait Notifier extends RepositoryService with AccountService with IssuesService { + def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) + (msg: String => String)(implicit context: Context): Unit + + protected def recipients(issue: model.Issue)(notify: String => Unit)(implicit context: Context) = + ( + // individual repository's owner + issue.userName :: + // collaborators + getCollaborators(issue.userName, issue.repositoryName) ::: + // participants + issue.openedUserName :: + getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName) + ) + .distinct + .withFilter ( _ != context.loginAccount.get.userName ) // the operation in person is excluded + .foreach ( getAccountByUserName(_) filterNot (_.isGroupAccount) foreach (x => notify(x.mailAddress)) ) + +} + +object Notifier { + // TODO We want to be able to switch to mock. + def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match { + case settings if settings.notification => new Mailer(settings.smtp.get) + case _ => new MockMailer + } + + def msgIssue(url: String) = (content: String) => s""" + |${content}
+ |--
+ |View it on GitBucket + """.stripMargin + + def msgPullRequest(url: String) = (content: String) => s""" + |${content}
+ |View, comment on, or merge it at:
+ |${url} + """.stripMargin + + def msgComment(url: String) = (content: String) => s""" + |${content}
+ |--
+ |View it on GitBucket + """.stripMargin + + def msgStatus(url: String) = (content: String) => s""" + |${content} #${url split('/') last} + """.stripMargin +} + +class Mailer(private val smtp: Smtp) extends Notifier { + private val logger = LoggerFactory.getLogger(classOf[Mailer]) + + def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) + (msg: String => String)(implicit context: Context) = { + val database = Database(context.request.getServletContext) + + val f = future { + // TODO Can we use the Database Session in other than Transaction Filter? + database withSession { + getIssue(r.owner, r.name, issueId.toString) foreach { issue => + defining( + s"[${r.name}] ${issue.title} (#${issueId})" -> + msg(view.Markdown.toHtml(content, r, false, true))) { case (subject, msg) => + recipients(issue) { to => + val email = new HtmlEmail + email.setHostName(smtp.host) + email.setSmtpPort(smtp.port.get) + smtp.user.foreach { user => + email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse(""))) + } + smtp.ssl.foreach { ssl => + email.setSSLOnConnect(ssl) + } + smtp.fromAddress + .map (_ -> smtp.fromName.orNull) + .orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName)) + .foreach { case (address, name) => + email.setFrom(address, name) + } + email.setCharset("UTF-8") + email.setSubject(subject) + email.setHtmlMsg(msg) + + email.addTo(to).send + } + } + } + } + "Notifications Successful." + } + f onSuccess { + case s => logger.debug(s) + } + f onFailure { + case t => logger.error("Notifications Failed.", t) + } + } +} +class MockMailer extends Notifier { + def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) + (msg: String => String)(implicit context: Context): Unit = {} } \ No newline at end of file diff --git a/src/main/twirl/admin/users/list.scala.html b/src/main/twirl/admin/users/list.scala.html index 942f6ee..3b2029c 100644 --- a/src/main/twirl/admin/users/list.scala.html +++ b/src/main/twirl/admin/users/list.scala.html @@ -1,71 +1,71 @@ -@(users: List[model.Account], members: Map[String, List[String]], includeRemoved: Boolean)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main("Manage Users"){ - @admin.html.menu("users"){ -
- New User - New Group -
- - - @users.map { account => - - - - } -
-
- @if(account.isGroupAccount){ - Edit - } else { - Edit - } -
-
- @avatar(account.userName, 20) - @account.userName - @if(account.isGroupAccount){ - (Group) - } else { - @if(account.isAdmin){ - (Administrator) - } else { - (Normal) - } - } - @if(account.isGroupAccount){ - @members(account.userName).map { userName => - @avatar(userName, 20, tooltip = true) - } - } -
-
-
- @if(!account.isGroupAccount){ - @account.mailAddress - } - @account.url.map { url => - @url - } -
-
- Registered: @datetime(account.registeredDate) - Updated: @datetime(account.updatedDate) - @if(!account.isGroupAccount){ - Last Login: @account.lastLoginDate.map(datetime) - } -
-
- } -} - \ No newline at end of file diff --git a/src/main/twirl/admin/users/user.scala.html b/src/main/twirl/admin/users/user.scala.html index 3a2430c..fb022c0 100644 --- a/src/main/twirl/admin/users/user.scala.html +++ b/src/main/twirl/admin/users/user.scala.html @@ -1,80 +1,80 @@ -@(account: Option[model.Account])(implicit context: app.Context) -@import context._ -@html.main(if(account.isEmpty) "New User" else "Update User"){ - @admin.html.menu("users"){ -
-
-
-
- -
- -
- - @if(account.isDefined){ - - } -
- @if(account.map(_.password.nonEmpty).getOrElse(true)){ -
- -
- -
- -
- } -
- -
- -
- -
-
- -
- -
- -
-
- - - -
-
- -
- -
- -
-
-
-
- - @helper.html.uploadavatar(account) -
-
-
-
- - Cancel -
-
- } -} +@(account: Option[model.Account])(implicit context: app.Context) +@import context._ +@html.main(if(account.isEmpty) "New User" else "Update User"){ + @admin.html.menu("users"){ +
+
+
+
+ +
+ +
+ + @if(account.isDefined){ + + } +
+ @if(account.map(_.password.nonEmpty).getOrElse(true)){ +
+ +
+ +
+ +
+ } +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ + + +
+
+ +
+ +
+ +
+
+
+
+ + @helper.html.uploadavatar(account) +
+
+
+
+ + Cancel +
+
+ } +} diff --git a/src/main/twirl/helper/activities.scala.html b/src/main/twirl/helper/activities.scala.html index 6e7f324..980eaf5 100644 --- a/src/main/twirl/helper/activities.scala.html +++ b/src/main/twirl/helper/activities.scala.html @@ -1,98 +1,98 @@ -@(activities: List[model.Activity])(implicit context: app.Context) -@import context._ -@import view.helpers._ - -@if(activities.isEmpty){ - No activity -} else { - @activities.map { activity => -
- @(activity.activityType match { - case "open_issue" => detailActivity(activity, "activity-issue.png") - case "comment_issue" => detailActivity(activity, "activity-comment.png") - case "close_issue" => detailActivity(activity, "activity-issue-close.png") - case "reopen_issue" => detailActivity(activity, "activity-issue-reopen.png") - case "open_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_branch" => simpleActivity(activity, "activity-branch.png") - case "delete_branch" => simpleActivity(activity, "activity-delete.png") - case "create_tag" => simpleActivity(activity, "activity-tag.png") - case "delete_tag" => simpleActivity(activity, "activity-delete.png") - case "fork" => simpleActivity(activity, "activity-fork.png") - case "push" => customActivity(activity, "activity-commit.png"){ -
- {activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) => - if(i == 3){ -
...
- } else { - if(commit.nonEmpty){ -
- {commit.substring(0, 7)} - {commit.substring(41)} -
- } - } - }} -
- } - case "create_wiki" => customActivity(activity, "activity-wiki.png"){ -
- Created {activity.additionalInfo.get}. -
- } - case "edit_wiki" => customActivity(activity, "activity-wiki.png"){ - activity.additionalInfo.get.split(":") match { - case Array(pageName, commitId) => -
- Edited {pageName}. - View the diff ยป -
- case Array(pageName) => -
- Edited {pageName}. -
- } - } - }) -
- } -} - -@detailActivity(activity: model.Activity, image: String) = { -
-
-
@datetime(activity.activityDate)
-
- @avatar(activity.activityUserName, 16) - @activityMessage(activity.message) -
- @activity.additionalInfo.map { additionalInfo => -
@additionalInfo
- } -
-} - -@customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = { -
-
-
@datetime(activity.activityDate)
-
- @avatar(activity.activityUserName, 16) - @activityMessage(activity.message) -
- @additionalInfo -
-} - -@simpleActivity(activity: model.Activity, image: String) = { -
-
-
- @avatar(activity.activityUserName, 16) - @activityMessage(activity.message) - @datetime(activity.activityDate) -
-
-} - +@(activities: List[model.Activity])(implicit context: app.Context) +@import context._ +@import view.helpers._ + +@if(activities.isEmpty){ + No activity +} else { + @activities.map { activity => +
+ @(activity.activityType match { + case "open_issue" => detailActivity(activity, "activity-issue.png") + case "comment_issue" => detailActivity(activity, "activity-comment.png") + case "close_issue" => detailActivity(activity, "activity-issue-close.png") + case "reopen_issue" => detailActivity(activity, "activity-issue-reopen.png") + case "open_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_branch" => simpleActivity(activity, "activity-branch.png") + case "delete_branch" => simpleActivity(activity, "activity-delete.png") + case "create_tag" => simpleActivity(activity, "activity-tag.png") + case "delete_tag" => simpleActivity(activity, "activity-delete.png") + case "fork" => simpleActivity(activity, "activity-fork.png") + case "push" => customActivity(activity, "activity-commit.png"){ +
+ {activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) => + if(i == 3){ +
...
+ } else { + if(commit.nonEmpty){ +
+ {commit.substring(0, 7)} + {commit.substring(41)} +
+ } + } + }} +
+ } + case "create_wiki" => customActivity(activity, "activity-wiki.png"){ +
+ Created {activity.additionalInfo.get}. +
+ } + case "edit_wiki" => customActivity(activity, "activity-wiki.png"){ + activity.additionalInfo.get.split(":") match { + case Array(pageName, commitId) => +
+ Edited {pageName}. + View the diff ยป +
+ case Array(pageName) => +
+ Edited {pageName}. +
+ } + } + }) +
+ } +} + +@detailActivity(activity: model.Activity, image: String) = { +
+
+
@datetime(activity.activityDate)
+
+ @avatar(activity.activityUserName, 16) + @activityMessage(activity.message) +
+ @activity.additionalInfo.map { additionalInfo => +
@additionalInfo
+ } +
+} + +@customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = { +
+
+
@datetime(activity.activityDate)
+
+ @avatar(activity.activityUserName, 16) + @activityMessage(activity.message) +
+ @additionalInfo +
+} + +@simpleActivity(activity: model.Activity, image: String) = { +
+
+
+ @avatar(activity.activityUserName, 16) + @activityMessage(activity.message) + @datetime(activity.activityDate) +
+
+} + diff --git a/src/main/twirl/helper/diff.scala.html b/src/main/twirl/helper/diff.scala.html index 9b9e25a..e1ab2d7 100644 --- a/src/main/twirl/helper/diff.scala.html +++ b/src/main/twirl/helper/diff.scala.html @@ -1,105 +1,105 @@ -@(diffs: Seq[util.JGitUtil.DiffInfo], - repository: service.RepositoryService.RepositoryInfo, - newCommitId: Option[String], - oldCommitId: Option[String], - showIndex: Boolean)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@import org.eclipse.jgit.diff.DiffEntry.ChangeType -@if(showIndex){ -
-
- -
- Showing @diffs.size changed @plural(diffs.size, "file") -
- -} -@diffs.zipWithIndex.map { case (diff, i) => - - - - - - - - -
- @if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){ - @diff.oldPath -> @diff.newPath - @if(newCommitId.isDefined){ - - } - } - @if(diff.changeType == ChangeType.ADD || diff.changeType == ChangeType.MODIFY){ - @diff.newPath - @if(newCommitId.isDefined){ - - } - } - @if(diff.changeType == ChangeType.DELETE){ - @diff.oldPath - @if(oldCommitId.isDefined){ - - } - } -
- @if(diff.newContent != None || diff.oldContent != None){ -
- - - } else { - Not supported - } -
-} - - - - + + + \ No newline at end of file diff --git a/src/main/twirl/index.scala.html b/src/main/twirl/index.scala.html index d019510..ec8b7ce 100644 --- a/src/main/twirl/index.scala.html +++ b/src/main/twirl/index.scala.html @@ -1,70 +1,70 @@ -@(activities: List[model.Activity], - recentRepositories: List[service.RepositoryService.RepositoryInfo], - userRepositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context) -@import context._ -@import view.helpers._ -@main("GitBucket"){ -
- @dashboard.html.tab() -
-
- @helper.html.activities(activities) -
-
- @if(loginAccount.isEmpty){ - @signinform(settings) - } else { - - - - - @if(userRepositories.isEmpty){ - - - - } else { - @userRepositories.map { repository => - - - - } - } -
- - Your repositories (@userRepositories.size) -
No repositories
- @helper.html.repositoryicon(repository, false) - @if(repository.owner == loginAccount.get.userName){ - @repository.name - } else { - @repository.owner/@repository.name - } -
- } - - - - - @if(recentRepositories.isEmpty){ - - - - } else { - @recentRepositories.map { repository => - - - - } - } -
- Recent updated repositories -
No repositories
- @helper.html.repositoryicon(repository, false) - @repository.owner/@repository.name -
-
-
-
-} +@(activities: List[model.Activity], + recentRepositories: List[service.RepositoryService.RepositoryInfo], + userRepositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context) +@import context._ +@import view.helpers._ +@main("GitBucket"){ +
+ @dashboard.html.tab() +
+
+ @helper.html.activities(activities) +
+
+ @if(loginAccount.isEmpty){ + @signinform(settings) + } else { + + + + + @if(userRepositories.isEmpty){ + + + + } else { + @userRepositories.map { repository => + + + + } + } +
+ + Your repositories (@userRepositories.size) +
No repositories
+ @helper.html.repositoryicon(repository, false) + @if(repository.owner == loginAccount.get.userName){ + @repository.name + } else { + @repository.owner/@repository.name + } +
+ } + + + + + @if(recentRepositories.isEmpty){ + + + + } else { + @recentRepositories.map { repository => + + + + } + } +
+ Recent updated repositories +
No repositories
+ @helper.html.repositoryicon(repository, false) + @repository.owner/@repository.name +
+
+
+
+} diff --git a/src/main/twirl/issues/create.scala.html b/src/main/twirl/issues/create.scala.html index dcffc7e..95c5091 100644 --- a/src/main/twirl/issues/create.scala.html +++ b/src/main/twirl/issues/create.scala.html @@ -1,147 +1,147 @@ -@(collaborators: List[String], - milestones: List[model.Milestone], - labels: List[model.Label], - hasWritePermission: Boolean, - repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){ - @html.menu("issues", repository){ - @tab("", true, repository) -
-
-
-
@avatar(loginAccount.get.userName, 48)
-
-
- - -
- No one is assigned - @if(hasWritePermission){ - - @helper.html.dropdown() { -
  • Clear assignee
  • - @collaborators.map { collaborator => -
  • @avatar(collaborator, 20) @collaborator
  • - } - } - } -
    - No milestone - @if(hasWritePermission){ - - @helper.html.dropdown() { -
  • No milestone
  • - @milestones.filter(_.closedDate.isEmpty).map { milestone => -
  • - - @milestone.title -
    - @milestone.dueDate.map { dueDate => - @if(isPast(dueDate)){ - Due in @date(dueDate) - } else { - Due in @date(dueDate) - } - }.getOrElse { - No due date - } -
    -
    -
  • - } - } - } -
    -
    -
    - @helper.html.preview(repository, "", false, true, "width: 565px; height: 200px; max-height: 250px;", elastic = true) -
    -
    -
    - -
    -
    -
    - @if(hasWritePermission){ - Add Labels -
    -
    - - -
    -
    - } -
    -
    -
    - } -} - +@(collaborators: List[String], + milestones: List[model.Milestone], + labels: List[model.Label], + hasWritePermission: Boolean, + repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) +@import context._ +@import view.helpers._ +@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){ + @html.menu("issues", repository){ + @tab("", true, repository) +
    +
    +
    +
    @avatar(loginAccount.get.userName, 48)
    +
    +
    + + +
    + No one is assigned + @if(hasWritePermission){ + + @helper.html.dropdown() { +
  • Clear assignee
  • + @collaborators.map { collaborator => +
  • @avatar(collaborator, 20) @collaborator
  • + } + } + } +
    + No milestone + @if(hasWritePermission){ + + @helper.html.dropdown() { +
  • No milestone
  • + @milestones.filter(_.closedDate.isEmpty).map { milestone => +
  • + + @milestone.title +
    + @milestone.dueDate.map { dueDate => + @if(isPast(dueDate)){ + Due in @date(dueDate) + } else { + Due in @date(dueDate) + } + }.getOrElse { + No due date + } +
    +
    +
  • + } + } + } +
    +
    +
    + @helper.html.preview(repository, "", false, true, "width: 565px; height: 200px; max-height: 250px;", elastic = true) +
    +
    +
    + +
    +
    +
    + @if(hasWritePermission){ + Add Labels +
    +
    + + +
    +
    + } +
    +
    +
    + } +} + diff --git a/src/main/twirl/pulls/commits.scala.html b/src/main/twirl/pulls/commits.scala.html index 9ee31e2..68636d5 100644 --- a/src/main/twirl/pulls/commits.scala.html +++ b/src/main/twirl/pulls/commits.scala.html @@ -1,25 +1,25 @@ -@(commits: Seq[Seq[util.JGitUtil.CommitInfo]], - repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -
    - - @commits.map { day => - - - - @day.map { commit => - - - - - - } - } -
    @date(day.head.time)
    - @avatar(commit, 20) - @user(commit.committer, commit.mailAddress, "username") - @commit.shortMessage - @commit.id.substring(0, 7) -
    -
    +@(commits: Seq[Seq[util.JGitUtil.CommitInfo]], + repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) +@import context._ +@import view.helpers._ +
    + + @commits.map { day => + + + + @day.map { commit => + + + + + + } + } +
    @date(day.head.time)
    + @avatar(commit, 20) + @user(commit.committer, commit.mailAddress, "username") + @commit.shortMessage + @commit.id.substring(0, 7) +
    +
    diff --git a/src/main/twirl/repo/tags.scala.html b/src/main/twirl/repo/tags.scala.html index 57a8a69..a04da07 100644 --- a/src/main/twirl/repo/tags.scala.html +++ b/src/main/twirl/repo/tags.scala.html @@ -1,24 +1,24 @@ -@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { - @html.menu("code", repository){ -

    Tags

    - - - - - - - - @repository.tags.map { tag => - - - - - - - } -
    TagDateCommitDownload
    @tag.name@datetime(tag.time)@tag.id.substring(0, 10)ZIP
    - } +@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) +@import context._ +@import view.helpers._ +@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { + @html.menu("code", repository){ +

    Tags

    + + + + + + + + @repository.tags.map { tag => + + + + + + + } +
    TagDateCommitDownload
    @tag.name@datetime(tag.time)@tag.id.substring(0, 10)ZIP
    + } } \ No newline at end of file diff --git a/src/main/twirl/search/menu.scala.html b/src/main/twirl/search/menu.scala.html index adc9156..c66b6ab 100644 --- a/src/main/twirl/search/menu.scala.html +++ b/src/main/twirl/search/menu.scala.html @@ -1,38 +1,38 @@ -@(active: String, fileCount: Int, issueCount: Int, query: String, - repository: service.RepositoryService.RepositoryInfo)(body: Html)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.menu("", repository){ -
    -
    - -
    -
    -
    - - - -
    - @body -
    -
    +@(active: String, fileCount: Int, issueCount: Int, query: String, + repository: service.RepositoryService.RepositoryInfo)(body: Html)(implicit context: app.Context) +@import context._ +@import view.helpers._ +@html.menu("", repository){ +
    +
    + +
    +
    +
    + + + +
    + @body +
    +
    } \ No newline at end of file diff --git a/src/main/twirl/settings/collaborators.scala.html b/src/main/twirl/settings/collaborators.scala.html index 28efeb6..e225153 100644 --- a/src/main/twirl/settings/collaborators.scala.html +++ b/src/main/twirl/settings/collaborators.scala.html @@ -1,35 +1,35 @@ -@(collaborators: List[String], - isGroupRepository: Boolean, - repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main("Settings", Some(repository)){ - @html.menu("settings", repository){ - @menu("collaborators", repository){ -

    Manage Collaborators

    - - @if(!isGroupRepository){ -
    -
    - -
    - @helper.html.account("userName", 300) - -
    - } - } - } -} +@(collaborators: List[String], + isGroupRepository: Boolean, + repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) +@import context._ +@import view.helpers._ +@html.main("Settings", Some(repository)){ + @html.menu("settings", repository){ + @menu("collaborators", repository){ +

    Manage Collaborators

    + + @if(!isGroupRepository){ +
    +
    + +
    + @helper.html.account("userName", 300) + +
    + } + } + } +} diff --git a/src/main/twirl/settings/menu.scala.html b/src/main/twirl/settings/menu.scala.html index 616f4cf..5ce349f 100644 --- a/src/main/twirl/settings/menu.scala.html +++ b/src/main/twirl/settings/menu.scala.html @@ -1,26 +1,26 @@ -@(active: String, repository: service.RepositoryService.RepositoryInfo)(body: Html)(implicit context: app.Context) -@import context._ -@import view.helpers._ -
    -
    -
    - -
    -
    -
    - @body -
    -
    +@(active: String, repository: service.RepositoryService.RepositoryInfo)(body: Html)(implicit context: app.Context) +@import context._ +@import view.helpers._ +
    +
    +
    + +
    +
    +
    + @body +
    +
    diff --git a/src/main/twirl/settings/options.scala.html b/src/main/twirl/settings/options.scala.html index b6949dc..1483b43 100644 --- a/src/main/twirl/settings/options.scala.html +++ b/src/main/twirl/settings/options.scala.html @@ -1,100 +1,100 @@ -@(repository: service.RepositoryService.RepositoryInfo, info: Option[Any])(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main("Settings", Some(repository)){ - @html.menu("settings", repository){ - @menu("options", repository){ - @helper.html.information(info) -
    -
    -
    Settings
    -
    -
    - - - -
    -
    - - -
    -
    - - - @if(repository.branchList.isEmpty){ - - } - -
    -
    - -
    -
    - -
    -
    -
    - @* -
    -
    Features:
    -
    -
    -
    - -
    -
    - Adds lightweight Wiki system to this repository. - This is the simplest way to provide documentation or examples. - Only collaborators can edit Wiki pages. -
    -
    -
    -
    -
    - -
    -
    - Adds lightweight issue tracking integrated with this repository. - All users who have signed in and can access this repository can register an issue. -
    -
    -
    -
    - *@ -
    - -
    -
    - } - } -} +@(repository: service.RepositoryService.RepositoryInfo, info: Option[Any])(implicit context: app.Context) +@import context._ +@import view.helpers._ +@html.main("Settings", Some(repository)){ + @html.menu("settings", repository){ + @menu("options", repository){ + @helper.html.information(info) +
    +
    +
    Settings
    +
    +
    + + + +
    +
    + + +
    +
    + + + @if(repository.branchList.isEmpty){ + + } + +
    +
    + +
    +
    + +
    +
    +
    + @* +
    +
    Features:
    +
    +
    +
    + +
    +
    + Adds lightweight Wiki system to this repository. + This is the simplest way to provide documentation or examples. + Only collaborators can edit Wiki pages. +
    +
    +
    +
    +
    + +
    +
    + Adds lightweight issue tracking integrated with this repository. + All users who have signed in and can access this repository can register an issue. +
    +
    +
    +
    + *@ +
    + +
    +
    + } + } +} diff --git a/src/main/twirl/signin.scala.html b/src/main/twirl/signin.scala.html index f70e844..c7eec08 100644 --- a/src/main/twirl/signin.scala.html +++ b/src/main/twirl/signin.scala.html @@ -1,7 +1,7 @@ -@()(implicit context: app.Context) -@import context._ -@main("Sign in"){ -
    - @signinform(settings) -
    -} +@()(implicit context: app.Context) +@import context._ +@main("Sign in"){ +
    + @signinform(settings) +
    +} diff --git a/src/main/twirl/wiki/compare.scala.html b/src/main/twirl/wiki/compare.scala.html index 190a824..3143676 100644 --- a/src/main/twirl/wiki/compare.scala.html +++ b/src/main/twirl/wiki/compare.scala.html @@ -1,40 +1,40 @@ -@(pageName: Option[String], - from: String, - to: String, - diffs: Seq[util.JGitUtil.DiffInfo], - repository: service.RepositoryService.RepositoryInfo, - hasWritePermission: Boolean, - info: Option[Any])(implicit context: app.Context) -@import context._ -@import view.helpers._ -@import org.eclipse.jgit.diff.DiffEntry.ChangeType -@html.main(s"Compare Revisions - ${repository.owner}/${repository.name}", Some(repository)){ - @helper.html.information(info) - @html.menu("wiki", repository){ - - @helper.html.diff(diffs, repository, None, None, false) - @if(hasWritePermission){ -
    - @if(pageName.isDefined){ - Revert Changes - } else { - Revert Changes - } -
    - } - } -} +@(pageName: Option[String], + from: String, + to: String, + diffs: Seq[util.JGitUtil.DiffInfo], + repository: service.RepositoryService.RepositoryInfo, + hasWritePermission: Boolean, + info: Option[Any])(implicit context: app.Context) +@import context._ +@import view.helpers._ +@import org.eclipse.jgit.diff.DiffEntry.ChangeType +@html.main(s"Compare Revisions - ${repository.owner}/${repository.name}", Some(repository)){ + @helper.html.information(info) + @html.menu("wiki", repository){ + + @helper.html.diff(diffs, repository, None, None, false) + @if(hasWritePermission){ +
    + @if(pageName.isDefined){ + Revert Changes + } else { + Revert Changes + } +
    + } + } +} diff --git a/src/main/webapp/assets/common/js/gitbucket.js b/src/main/webapp/assets/common/js/gitbucket.js index 94de1c4..93e832e 100644 --- a/src/main/webapp/assets/common/js/gitbucket.js +++ b/src/main/webapp/assets/common/js/gitbucket.js @@ -1,109 +1,109 @@ -$(function(){ - // disable Ajax cache - $.ajaxSetup({ cache: false }); - - // repository url text field - $('#repository-url').click(function(){ - this.select(0, this.value.length); - }); - - // activate tooltip - $('img[data-toggle=tooltip]').tooltip(); - $('a[data-toggle=tooltip]').tooltip(); - - // anchor icon for markdown - $('.markdown-head').mouseenter(function(e){ - $(e.target).children('a.markdown-anchor-link').show(); - }); - $('.markdown-head').mouseleave(function(e){ - var anchorLink = $(e.target).children('a.markdown-anchor-link'); - if(anchorLink.data('active') != true){ - anchorLink.hide(); - } - }); - - $('a.markdown-anchor-link').mouseenter(function(e){ - $(e.target).data('active', true); - }); - - $('a.markdown-anchor-link').mouseleave(function(e){ - $(e.target).data('active', false); - $(e.target).hide(); - }); - - // syntax highlighting by google-code-prettify - prettyPrint(); -}); - -function displayErrors(data){ - var i = 0; - $.each(data, function(key, value){ - $('#error-' + key.split(".").join("_")).text(value); - if(i == 0){ - $('#' + key).focus(); - } - i++; - }); -} - -(function($){ - $.fn.watch = function(callback){ - var timer = null; - var prevValue = this.val(); - - this.on('focus', function(e){ - window.clearInterval(timer); - timer = window.setInterval(function(){ - var newValue = $(e.target).val(); - if(prevValue != newValue){ - callback(); - } - prevValue = newValue; - }, 10); - }); - - this.on('blur', function(){ - window.clearInterval(timer); - }); - }; -})(jQuery); - -function diffUsingJS(oldTextId, newTextId, outputId) { - // get the baseText and newText values from the two textboxes, and split them into lines - var oldText = document.getElementById(oldTextId).value; - if(oldText == ''){ - var oldLines = []; - } else { - var oldLines = difflib.stringAsLines(oldText); - } - - var newText = document.getElementById(newTextId).value - if(newText == ''){ - var newLines = []; - } else { - var newLines = difflib.stringAsLines(newText); - } - - // create a SequenceMatcher instance that diffs the two sets of lines - var sm = new difflib.SequenceMatcher(oldLines, newLines); - - // get the opcodes from the SequenceMatcher instance - // opcodes is a list of 3-tuples describing what changes should be made to the base text - // in order to yield the new text - var opcodes = sm.get_opcodes(); - var diffoutputdiv = document.getElementById(outputId); - while (diffoutputdiv.firstChild) diffoutputdiv.removeChild(diffoutputdiv.firstChild); - - // build the diff view and add it to the current DOM - diffoutputdiv.appendChild(diffview.buildView({ - baseTextLines: oldLines, - newTextLines: newLines, - opcodes: opcodes, - contextSize: 4, - viewType: 1 - })); -} - -function jqSelectorEscape(val) { - return val.replace(/[!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~]/g, '\\$&'); +$(function(){ + // disable Ajax cache + $.ajaxSetup({ cache: false }); + + // repository url text field + $('#repository-url').click(function(){ + this.select(0, this.value.length); + }); + + // activate tooltip + $('img[data-toggle=tooltip]').tooltip(); + $('a[data-toggle=tooltip]').tooltip(); + + // anchor icon for markdown + $('.markdown-head').mouseenter(function(e){ + $(e.target).children('a.markdown-anchor-link').show(); + }); + $('.markdown-head').mouseleave(function(e){ + var anchorLink = $(e.target).children('a.markdown-anchor-link'); + if(anchorLink.data('active') != true){ + anchorLink.hide(); + } + }); + + $('a.markdown-anchor-link').mouseenter(function(e){ + $(e.target).data('active', true); + }); + + $('a.markdown-anchor-link').mouseleave(function(e){ + $(e.target).data('active', false); + $(e.target).hide(); + }); + + // syntax highlighting by google-code-prettify + prettyPrint(); +}); + +function displayErrors(data){ + var i = 0; + $.each(data, function(key, value){ + $('#error-' + key.split(".").join("_")).text(value); + if(i == 0){ + $('#' + key).focus(); + } + i++; + }); +} + +(function($){ + $.fn.watch = function(callback){ + var timer = null; + var prevValue = this.val(); + + this.on('focus', function(e){ + window.clearInterval(timer); + timer = window.setInterval(function(){ + var newValue = $(e.target).val(); + if(prevValue != newValue){ + callback(); + } + prevValue = newValue; + }, 10); + }); + + this.on('blur', function(){ + window.clearInterval(timer); + }); + }; +})(jQuery); + +function diffUsingJS(oldTextId, newTextId, outputId) { + // get the baseText and newText values from the two textboxes, and split them into lines + var oldText = document.getElementById(oldTextId).value; + if(oldText == ''){ + var oldLines = []; + } else { + var oldLines = difflib.stringAsLines(oldText); + } + + var newText = document.getElementById(newTextId).value + if(newText == ''){ + var newLines = []; + } else { + var newLines = difflib.stringAsLines(newText); + } + + // create a SequenceMatcher instance that diffs the two sets of lines + var sm = new difflib.SequenceMatcher(oldLines, newLines); + + // get the opcodes from the SequenceMatcher instance + // opcodes is a list of 3-tuples describing what changes should be made to the base text + // in order to yield the new text + var opcodes = sm.get_opcodes(); + var diffoutputdiv = document.getElementById(outputId); + while (diffoutputdiv.firstChild) diffoutputdiv.removeChild(diffoutputdiv.firstChild); + + // build the diff view and add it to the current DOM + diffoutputdiv.appendChild(diffview.buildView({ + baseTextLines: oldLines, + newTextLines: newLines, + opcodes: opcodes, + contextSize: 4, + viewType: 1 + })); +} + +function jqSelectorEscape(val) { + return val.replace(/[!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~]/g, '\\$&'); } \ No newline at end of file diff --git a/src/main/webapp/assets/common/js/validation.js b/src/main/webapp/assets/common/js/validation.js index 135fb82..9be17d0 100644 --- a/src/main/webapp/assets/common/js/validation.js +++ b/src/main/webapp/assets/common/js/validation.js @@ -1,40 +1,40 @@ -$(function(){ - $.each($('form[validate=true]'), function(i, form){ - $(form).submit(validate); - }); - $.each($('input[formaction]'), function(i, input){ - $(input).click(function(){ - var form = $(input).parents('form') - $(form).attr('action', $(input).attr('formaction')) - }); - }); -}); - -function validate(e){ - var form = $(e.target); - - if(form.data('validated') == true){ - return true; - } - - $.post(form.attr('action') + '/validate', $(e.target).serialize(), function(data){ - // clear all error messages - $('.error').text(''); - - if($.isEmptyObject(data)){ - form.data('validated', true); - form.submit(); - form.data('validated', false); - } else { - form.data('validated', false); - displayErrors(data); - } - }, 'json'); - return false; -} - -function displayErrors(data){ - $.each(data, function(key, value){ - $('#error-' + key.split(".").join("_")).text(value); - }); +$(function(){ + $.each($('form[validate=true]'), function(i, form){ + $(form).submit(validate); + }); + $.each($('input[formaction]'), function(i, input){ + $(input).click(function(){ + var form = $(input).parents('form') + $(form).attr('action', $(input).attr('formaction')) + }); + }); +}); + +function validate(e){ + var form = $(e.target); + + if(form.data('validated') == true){ + return true; + } + + $.post(form.attr('action') + '/validate', $(e.target).serialize(), function(data){ + // clear all error messages + $('.error').text(''); + + if($.isEmptyObject(data)){ + form.data('validated', true); + form.submit(); + form.data('validated', false); + } else { + form.data('validated', false); + displayErrors(data); + } + }, 'json'); + return false; +} + +function displayErrors(data){ + $.each(data, function(key, value){ + $('#error-' + key.split(".").join("_")).text(value); + }); } \ No newline at end of file diff --git a/src/main/webapp/assets/vendors/ace/mode-diff.js b/src/main/webapp/assets/vendors/ace/mode-diff.js index 8c919ea..7cfa96e 100644 --- a/src/main/webapp/assets/vendors/ace/mode-diff.js +++ b/src/main/webapp/assets/vendors/ace/mode-diff.js Binary files differ diff --git a/src/main/webapp/assets/vendors/ace/mode-jsoniq.js b/src/main/webapp/assets/vendors/ace/mode-jsoniq.js index 094a3a6..cc17fee 100644 --- a/src/main/webapp/assets/vendors/ace/mode-jsoniq.js +++ b/src/main/webapp/assets/vendors/ace/mode-jsoniq.js Binary files differ diff --git a/src/main/webapp/assets/vendors/ace/mode-xquery.js b/src/main/webapp/assets/vendors/ace/mode-xquery.js index a8e2869..b59ae69 100644 --- a/src/main/webapp/assets/vendors/ace/mode-xquery.js +++ b/src/main/webapp/assets/vendors/ace/mode-xquery.js Binary files differ diff --git a/src/main/webapp/assets/vendors/ace/worker-html.js b/src/main/webapp/assets/vendors/ace/worker-html.js index 7e2fc82..f295b35 100644 --- a/src/main/webapp/assets/vendors/ace/worker-html.js +++ b/src/main/webapp/assets/vendors/ace/worker-html.js Binary files differ diff --git a/src/main/webapp/assets/vendors/ace/worker-xquery.js b/src/main/webapp/assets/vendors/ace/worker-xquery.js index 31a1c71..457acf0 100644 --- a/src/main/webapp/assets/vendors/ace/worker-xquery.js +++ b/src/main/webapp/assets/vendors/ace/worker-xquery.js Binary files differ