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.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 @@ 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 {
val userName = params("userName")
getAccountByUserName(userName).map { x =>

View File

@@ -9,6 +9,8 @@ import util.Implicits._
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 @@ 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 =>
defining(repository.owner, repository.name){ case (owner, name) =>
issues.html.create(
@@ -163,6 +177,20 @@ trait IssuesControllerBase extends ControllerBase {
} 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}/${

View File

@@ -15,7 +15,7 @@ import service.PullRequestService._
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 @@ 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 =>
params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner
@@ -95,6 +113,47 @@ trait PullRequestsControllerBase extends ControllerBase {
} 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

View File

@@ -17,7 +17,7 @@ import org.eclipse.jgit.treewalk._
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
@@ -105,6 +105,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
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.
*/

View File

@@ -20,6 +20,12 @@ trait IssuesService {
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 @@ trait IssuesService {
(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 @@ trait IssuesService {
}} 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.
*/

View File

@@ -167,13 +167,14 @@ object WebHookService {
.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 @@ object WebHookService {
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 @@ object WebHookService {
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 @@ object WebHookService {
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 @@ object WebHookService {
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)
}
}

View File

@@ -14,6 +14,8 @@ object Implicits {
// 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 @@ object Implicits {
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