diff --git a/src/main/scala/app/AccountController.scala b/src/main/scala/app/AccountController.scala index 62711b5..c0b3c89 100644 --- a/src/main/scala/app/AccountController.scala +++ b/src/main/scala/app/AccountController.scala @@ -14,16 +14,17 @@ import org.eclipse.jgit.lib.{FileMode, Constants} import org.eclipse.jgit.dircache.DirCache import model.GroupMember +import service.WebHookService._ class AccountController extends AccountControllerBase with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator - with AccessTokenService + with AccessTokenService with WebHookService trait AccountControllerBase extends AccountManagementControllerBase { self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator - with AccessTokenService => + with AccessTokenService with WebHookService => case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String, url: Option[String], fileId: Option[String]) @@ -150,6 +151,25 @@ } } + /** + * https://developer.github.com/v3/users/#get-a-single-user + */ + get("/api/v3/users/:userName") { + getAccountByUserName(params("userName")).map { account => + apiJson(WebHookApiUser(account)) + } getOrElse NotFound + } + + /** + * https://developer.github.com/v3/users/#get-the-authenticated-user + */ + get("/api/v3/user") { + context.loginAccount.map { account => + apiJson(WebHookApiUser(account)) + } getOrElse NotFound + } + + get("/:userName/_edit")(oneselfOnly { val userName = params("userName") getAccountByUserName(userName).map { x => diff --git a/src/main/scala/app/IssuesController.scala b/src/main/scala/app/IssuesController.scala index 3dd3ec7..af4dc16 100644 --- a/src/main/scala/app/IssuesController.scala +++ b/src/main/scala/app/IssuesController.scala @@ -9,6 +9,8 @@ import util.ControlUtil._ import org.scalatra.Ok import model.Issue +import service.WebHookService._ +import scala.util.Try class IssuesController extends IssuesControllerBase with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService @@ -73,6 +75,18 @@ } }) + /** + * https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue + */ + get("/api/v3/repos/:owner/:repository/issues/:id/comments")(referrersOnly { repository => + (for{ + issueId <- params("id").toIntOpt + comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt) + } yield { + apiJson(comments.map{ case (issueComment, user) => WebHookComment(issueComment, WebHookApiUser(user)) }) + }).getOrElse(NotFound) + }) + get("/:owner/:repository/issues/new")(readableUsersOnly { repository => defining(repository.owner, repository.name){ case (owner, name) => issues.html.create( @@ -163,6 +177,20 @@ } getOrElse NotFound }) + /** + * https://developer.github.com/v3/issues/comments/#create-a-comment + */ + post("/api/v3/repos/:owner/:repository/issues/:id/comments")(readableUsersOnly { repository => + val data = multiParams.keys.headOption.flatMap(b => Try(parse(b).extract[CreateAComment]).toOption) + (for{ + issueId <- params("id").toIntOpt + (issue, id) <- handleComment(issueId, data.map(_.body), repository)() + issueComment <- getComment(repository.owner, repository.name, id.toString()) + } yield { + apiJson(WebHookComment(issueComment, WebHookApiUser(context.loginAccount.get))) + }) 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}/${ diff --git a/src/main/scala/app/PullRequestsController.scala b/src/main/scala/app/PullRequestsController.scala index 83510f2..a828a1c 100644 --- a/src/main/scala/app/PullRequestsController.scala +++ b/src/main/scala/app/PullRequestsController.scala @@ -15,7 +15,7 @@ import org.slf4j.LoggerFactory import org.eclipse.jgit.merge.MergeStrategy import org.eclipse.jgit.errors.NoMergeBaseException -import service.WebHookService.{ WebHookPayload, WebHookPullRequestPayload } +import service.WebHookService._ import util.JGitUtil.DiffInfo import util.JGitUtil.CommitInfo import model.{PullRequest, Issue} @@ -69,6 +69,24 @@ } }) + /** + * https://developer.github.com/v3/pulls/#list-pull-requests + */ + get("/api/v3/repos/:owner/:repository/pulls")(referrersOnly { repository => + val page = IssueSearchCondition.page(request) + // TODO: more api spec condition + val condition = IssueSearchCondition(request) + val baseOwner = getAccountByUserName(repository.owner).get + val issues:List[(model.Issue, model.Account, Int, model.PullRequest, model.Repository, model.Account)] = searchPullRequestByApi(condition, (page - 1) * PullRequestLimit, PullRequestLimit, repository.owner -> repository.name) + apiJson(issues.map{case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner) => + WebHookPullRequest( + issue, + pullRequest, + WebHookRepository(headRepo, WebHookApiUser(headOwner)), + WebHookRepository(repository, WebHookApiUser(baseOwner)), + WebHookApiUser(issueUser)) }) + }) + get("/:owner/:repository/pull/:id")(referrersOnly { repository => params("id").toIntOpt.flatMap{ issueId => val owner = repository.owner @@ -95,6 +113,47 @@ } getOrElse NotFound }) + /** + * https://developer.github.com/v3/pulls/#get-a-single-pull-request + */ + get("/api/v3/repos/:owner/:repository/pulls/:id")(referrersOnly { repository => + (for{ + issueId <- params("id").toIntOpt + (issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId) + users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.userName), Set()) + baseOwner <- users.get(repository.owner) + headOwner <- users.get(pullRequest.requestUserName) + issueUser <- users.get(issue.userName) + headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl) + } yield { + apiJson(WebHookPullRequest( + issue, + pullRequest, + WebHookRepository(headRepo, WebHookApiUser(headOwner)), + WebHookRepository(repository, WebHookApiUser(baseOwner)), + WebHookApiUser(issueUser))) + }).getOrElse(NotFound) + }) + + /** + * https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request + */ + get("/api/v3/repos/:owner/:repository/pulls/:id/commits")(referrersOnly { repository => + val owner = repository.owner + val name = repository.name + params("id").toIntOpt.flatMap{ issueId => + getPullRequest(owner, name, issueId) map { case(issue, pullreq) => + using(Git.open(getRepositoryDir(owner, name))){ git => + val oldId = git.getRepository.resolve(pullreq.commitIdFrom) + val newId = git.getRepository.resolve(pullreq.commitIdTo) + val repoFullName = s"${owner}/${name}" + val commits = git.log.addRange(oldId, newId).call.iterator.asScala.map(c => WebHookCommitListItem(new CommitInfo(c), repoFullName)).toList + apiJson(commits) + } + } + } getOrElse NotFound + }) + ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository => params("id").toIntOpt.flatMap{ issueId => val owner = repository.owner diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala index bc6339b..de0b140 100644 --- a/src/main/scala/app/RepositoryViewerController.scala +++ b/src/main/scala/app/RepositoryViewerController.scala @@ -17,7 +17,7 @@ import jp.sf.amateras.scalatra.forms._ import org.eclipse.jgit.dircache.DirCache import org.eclipse.jgit.revwalk.RevCommit -import service.WebHookService.WebHookPushPayload +import service.WebHookService._ class RepositoryViewerController extends RepositoryViewerControllerBase with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService @@ -106,6 +106,13 @@ }) /** + * https://developer.github.com/v3/repos/#get + */ + get("/api/v3/repos/:owner/:repository")(referrersOnly { repository => + apiJson(WebHookRepository(repository, WebHookApiUser(getAccountByUserName(repository.owner).get))) + }) + + /** * Displays the file list of the specified path and branch. */ get("/:owner/:repository/tree/*")(referrersOnly { repository => diff --git a/src/main/scala/service/IssuesService.scala b/src/main/scala/service/IssuesService.scala index cf7ac00..582677e 100644 --- a/src/main/scala/service/IssuesService.scala +++ b/src/main/scala/service/IssuesService.scala @@ -20,6 +20,12 @@ def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) = IssueComments filter (_.byIssue(owner, repository, issueId)) list + def getCommentsForApi(owner: String, repository: String, issueId: Int)(implicit s: Session) = + IssueComments.filter(_.byIssue(owner, repository, issueId)) + .filter(_.action inSetBind Set("commant" , "close_comment", "reopen_comment")) + .innerJoin(Accounts).on( (t1, t2) => t1.userName === t2.userName ) + .list + def getComment(owner: String, repository: String, commentId: String)(implicit s: Session) = if (commentId forall (_.isDigit)) IssueComments filter { t => @@ -92,21 +98,7 @@ (implicit s: Session): List[IssueInfo] = { // get issues and comment count and labels - searchIssueQuery(repos, condition, pullRequest) - .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) + searchIssueQueryBase(condition, pullRequest, offset, limit, repos) .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) } .leftJoin (Milestones) .on { case ((((t1, t2), t3), t4), t5) => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) } @@ -130,6 +122,42 @@ }} toList } + /** for api + * @return (issue, commentCount, pullRequest, headRepository, headOwner) + */ + def searchPullRequestByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*) + (implicit s: Session): List[(Issue, model.Account, Int, model.PullRequest, model.Repository, model.Account)] = { + // get issues and comment count and labels + searchIssueQueryBase(condition, true, offset, limit, repos) + .innerJoin(PullRequests).on { case ((t1, t2), t3) => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) } + .innerJoin(Repositories).on { case (((t1, t2), t3), t4) => t4.byRepository(t1.userName, t1.repositoryName) } + .innerJoin(Accounts).on { case ((((t1, t2), t3), t4), t5) => t5.userName === t1.userName } + .innerJoin(Accounts).on { case (((((t1, t2), t3), t4), t5), t6) => t6.userName === t4.userName } + .map { case (((((t1, t2), t3), t4), t5), t6) => + (t1, t5, t2.commentCount, t3, t4, t6) + } + .list + } + + private def searchIssueQueryBase(condition: IssueSearchCondition, pullRequest: Boolean, offset: Int, limit: Int, repos: Seq[(String, String)]) + (implicit s: Session) = + searchIssueQuery(repos, condition, pullRequest) + .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) + + /** * Assembles query for conditional issue searching. */ diff --git a/src/main/scala/service/WebHookService.scala b/src/main/scala/service/WebHookService.scala index 909f32c..1a91af9 100644 --- a/src/main/scala/service/WebHookService.scala +++ b/src/main/scala/service/WebHookService.scala @@ -167,13 +167,14 @@ .getOrElse(throw new MappingException("Can't convert " + s + " to Date")) }, { case x: Date => JString(parserISO.print(new DateTime(x).withZone(DateTimeZone.UTC))) } ) - ) + FieldSerializer[WebHookApiUser]() + FieldSerializer[WebHookPullRequest]() + FieldSerializer[WebHookRepository]() + ) + FieldSerializer[WebHookApiUser]() + FieldSerializer[WebHookPullRequest]() + FieldSerializer[WebHookRepository]() + + FieldSerializer[WebHookCommitListItemParent]() + FieldSerializer[WebHookCommitListItem]() + FieldSerializer[WebHookCommitListItemCommit]() } def apiPathSerializer(c:ApiContext) = new CustomSerializer[ApiPath](format => ( { case JString(s) if s.startsWith(c.baseUrl) => ApiPath(s.substring(c.baseUrl.length)) - case JString(s) => throw new MappingException("Can't convert " + s + " to Date") + case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath") }, { case ApiPath(path) => JString(c.baseUrl+path) } ) @@ -293,14 +294,8 @@ removed = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.DELETE) => x.oldPath }, modified = diffs._1.collect { case x if(x.changeType != DiffEntry.ChangeType.ADD && x.changeType != DiffEntry.ChangeType.DELETE) => x.newPath }, - author = WebHookCommitUser( - name = commit.authorName, - email = commit.authorEmailAddress - ), - committer = WebHookCommitUser( - name = commit.committerName, - email = commit.committerEmailAddress - ) + author = WebHookCommitUser.author(commit), + committer = WebHookCommitUser.committer(commit) ) } } @@ -336,7 +331,21 @@ case class WebHookCommitUser( name: String, - email: String) + email: String, + date: Date) + + object WebHookCommitUser { + def author(commit: CommitInfo): WebHookCommitUser = + WebHookCommitUser( + name = commit.authorName, + email = commit.authorEmailAddress, + date = commit.authorTime) + def committer(commit: CommitInfo): WebHookCommitUser = + WebHookCommitUser( + name = commit.committerName, + email = commit.committerEmailAddress, + date = commit.commitTime) + } // https://developer.github.com/v3/repos/ case class WebHookRepository( @@ -417,7 +426,7 @@ sha = pullRequest.commitIdFrom, ref = pullRequest.branch, repo = baseRepo), - mergeable = None, + mergeable = Some(true), // TODO: need check mergeable. title = issue.title, body = issue.content.getOrElse(""), user = user @@ -483,4 +492,42 @@ created_at = comment.registeredDate, updated_at = comment.updatedDate) } + + // https://developer.github.com/v3/issues/comments/#create-a-comment + case class CreateAComment(body: String) + + + // https://developer.github.com/v3/repos/commits/ + case class WebHookCommitListItemParent(sha: String)(repoFullName:String){ + val url = ApiPath(s"/api/v3/repos/${repoFullName}/commits/${sha}") + } + case class WebHookCommitListItemCommit( + message: String, + author: WebHookCommitUser, + committer: WebHookCommitUser)(sha:String, repoFullName: String) { + val url = ApiPath(s"/api/v3/repos/${repoFullName}/git/commits/${sha}") + } + + case class WebHookCommitListItem( + sha: String, + commit: WebHookCommitListItemCommit, + author: Option[WebHookApiUser], + committer: Option[WebHookApiUser], + parents: Seq[WebHookCommitListItemParent])(repoFullName: String) { + val url = ApiPath(s"/api/v3/repos/${repoFullName}/commits/${sha}") + } + + object WebHookCommitListItem { + def apply(commit: CommitInfo, repoFullName:String): WebHookCommitListItem = WebHookCommitListItem( + sha = commit.id, + commit = WebHookCommitListItemCommit( + message = commit.fullMessage, + author = WebHookCommitUser.author(commit), + committer = WebHookCommitUser.committer(commit) + )(commit.id, repoFullName), + author = None, + committer = None, + parents = commit.parents.map(WebHookCommitListItemParent(_)(repoFullName)))(repoFullName) + } + } diff --git a/src/main/scala/util/Implicits.scala b/src/main/scala/util/Implicits.scala index f1b1115..c9c2919 100644 --- a/src/main/scala/util/Implicits.scala +++ b/src/main/scala/util/Implicits.scala @@ -14,6 +14,8 @@ // Convert to slick session. implicit def request2Session(implicit request: HttpServletRequest): JdbcBackend#Session = Database.getSession(request) + implicit def context2ApiContext(implicit context: app.Context): service.WebHookService.ApiContext = service.WebHookService.ApiContext(context.baseUrl) + implicit class RichSeq[A](seq: Seq[A]) { def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition) @@ -55,7 +57,10 @@ implicit class RichRequest(request: HttpServletRequest){ - def paths: Array[String] = request.getRequestURI.substring(request.getContextPath.length + 1).split("/") + def paths: Array[String] = (request.getRequestURI match{ + case path if path.startsWith("/api/v3/repos/") => path.substring(13/* "/api/v3/repos".length */) + case path => path + }).substring(request.getContextPath.length + 1).split("/") def hasQueryString: Boolean = request.getQueryString != null