add some WebAPI/v3.

* [Users](https://developer.github.com/v3/users/) get-a-single-user
 * [Issues/Comments](https://developer.github.com/v3/issues/comments/) list-comments-on-an-issue, create-a-comment
 * [Pull Requests](https://developer.github.com/v3/pulls/) list-pull-requests, get-a-single-pull-request, list-commits-on-a-pull-request
 * [Repositories](https://developer.github.com/v3/repos/) get
This commit is contained in:
nazoking
2015-01-19 23:26:49 +09:00
parent 32799cead7
commit 47cb4d1c49
7 changed files with 226 additions and 32 deletions

View File

@@ -14,16 +14,17 @@ import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.{FileMode, Constants} import org.eclipse.jgit.lib.{FileMode, Constants}
import org.eclipse.jgit.dircache.DirCache import org.eclipse.jgit.dircache.DirCache
import model.GroupMember import model.GroupMember
import service.WebHookService._
class AccountController extends AccountControllerBase class AccountController extends AccountControllerBase
with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
with AccessTokenService with AccessTokenService with WebHookService
trait AccountControllerBase extends AccountManagementControllerBase { trait AccountControllerBase extends AccountManagementControllerBase {
self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator 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, case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String]) url: Option[String], fileId: Option[String])
@@ -150,6 +151,25 @@ trait AccountControllerBase extends AccountManagementControllerBase {
} }
} }
/**
* 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 { get("/:userName/_edit")(oneselfOnly {
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName).map { x => getAccountByUserName(userName).map { x =>

View File

@@ -9,6 +9,8 @@ import util.Implicits._
import util.ControlUtil._ import util.ControlUtil._
import org.scalatra.Ok import org.scalatra.Ok
import model.Issue import model.Issue
import service.WebHookService._
import scala.util.Try
class IssuesController extends IssuesControllerBase class IssuesController extends IssuesControllerBase
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
@@ -73,6 +75,18 @@ trait IssuesControllerBase extends ControllerBase {
} }
}) })
/**
* 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 => get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
issues.html.create( issues.html.create(
@@ -163,6 +177,20 @@ trait IssuesControllerBase extends ControllerBase {
} getOrElse NotFound } 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) => post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
handleComment(form.issueId, form.content, repository)() map { case (issue, id) => handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
redirect(s"/${repository.owner}/${repository.name}/${ redirect(s"/${repository.owner}/${repository.name}/${

View File

@@ -15,7 +15,7 @@ import service.PullRequestService._
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.eclipse.jgit.merge.MergeStrategy import org.eclipse.jgit.merge.MergeStrategy
import org.eclipse.jgit.errors.NoMergeBaseException import org.eclipse.jgit.errors.NoMergeBaseException
import service.WebHookService.{ WebHookPayload, WebHookPullRequestPayload } import service.WebHookService._
import util.JGitUtil.DiffInfo import util.JGitUtil.DiffInfo
import util.JGitUtil.CommitInfo import util.JGitUtil.CommitInfo
import model.{PullRequest, Issue} import model.{PullRequest, Issue}
@@ -69,6 +69,24 @@ trait PullRequestsControllerBase extends ControllerBase {
} }
}) })
/**
* 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 => get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
params("id").toIntOpt.flatMap{ issueId => params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner val owner = repository.owner
@@ -95,6 +113,47 @@ trait PullRequestsControllerBase extends ControllerBase {
} getOrElse NotFound } 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 => ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository =>
params("id").toIntOpt.flatMap{ issueId => params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner val owner = repository.owner

View File

@@ -17,7 +17,7 @@ import org.eclipse.jgit.treewalk._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.dircache.DirCache import org.eclipse.jgit.dircache.DirCache
import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevCommit
import service.WebHookService.WebHookPushPayload import service.WebHookService._
class RepositoryViewerController extends RepositoryViewerControllerBase class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
@@ -105,6 +105,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
fileList(_) fileList(_)
}) })
/**
* 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. * Displays the file list of the specified path and branch.
*/ */

View File

@@ -20,6 +20,12 @@ trait IssuesService {
def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) = def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) =
IssueComments filter (_.byIssue(owner, repository, issueId)) list 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) = def getComment(owner: String, repository: String, commentId: String)(implicit s: Session) =
if (commentId forall (_.isDigit)) if (commentId forall (_.isDigit))
IssueComments filter { t => IssueComments filter { t =>
@@ -92,21 +98,7 @@ trait IssuesService {
(implicit s: Session): List[IssueInfo] = { (implicit s: Session): List[IssueInfo] = {
// get issues and comment count and labels // get issues and comment count and labels
searchIssueQuery(repos, condition, pullRequest) searchIssueQueryBase(condition, pullRequest, offset, limit, repos)
.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 (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 (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) } .leftJoin (Milestones) .on { case ((((t1, t2), t3), t4), t5) => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) }
@@ -130,6 +122,42 @@ trait IssuesService {
}} toList }} 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. * Assembles query for conditional issue searching.
*/ */

View File

@@ -167,13 +167,14 @@ object WebHookService {
.getOrElse(throw new MappingException("Can't convert " + s + " to Date")) }, .getOrElse(throw new MappingException("Can't convert " + s + " to Date")) },
{ case x: Date => JString(parserISO.print(new DateTime(x).withZone(DateTimeZone.UTC))) } { 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 => 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) 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) } { case ApiPath(path) => JString(c.baseUrl+path) }
) )
@@ -293,14 +294,8 @@ object WebHookService {
removed = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.DELETE) => x.oldPath }, 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 && modified = diffs._1.collect { case x if(x.changeType != DiffEntry.ChangeType.ADD &&
x.changeType != DiffEntry.ChangeType.DELETE) => x.newPath }, x.changeType != DiffEntry.ChangeType.DELETE) => x.newPath },
author = WebHookCommitUser( author = WebHookCommitUser.author(commit),
name = commit.authorName, committer = WebHookCommitUser.committer(commit)
email = commit.authorEmailAddress
),
committer = WebHookCommitUser(
name = commit.committerName,
email = commit.committerEmailAddress
)
) )
} }
} }
@@ -336,7 +331,21 @@ object WebHookService {
case class WebHookCommitUser( case class WebHookCommitUser(
name: String, 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/ // https://developer.github.com/v3/repos/
case class WebHookRepository( case class WebHookRepository(
@@ -417,7 +426,7 @@ object WebHookService {
sha = pullRequest.commitIdFrom, sha = pullRequest.commitIdFrom,
ref = pullRequest.branch, ref = pullRequest.branch,
repo = baseRepo), repo = baseRepo),
mergeable = None, mergeable = Some(true), // TODO: need check mergeable.
title = issue.title, title = issue.title,
body = issue.content.getOrElse(""), body = issue.content.getOrElse(""),
user = user user = user
@@ -483,4 +492,42 @@ object WebHookService {
created_at = comment.registeredDate, created_at = comment.registeredDate,
updated_at = comment.updatedDate) 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)
}
} }

View File

@@ -14,6 +14,8 @@ object Implicits {
// Convert to slick session. // Convert to slick session.
implicit def request2Session(implicit request: HttpServletRequest): JdbcBackend#Session = Database.getSession(request) 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]) { implicit class RichSeq[A](seq: Seq[A]) {
def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition) def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition)
@@ -55,7 +57,10 @@ object Implicits {
implicit class RichRequest(request: HttpServletRequest){ 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 def hasQueryString: Boolean = request.getQueryString != null