diff --git a/src/main/scala/app/PullRequestsController.scala b/src/main/scala/app/PullRequestsController.scala index a828a1c..1a3a2ca 100644 --- a/src/main/scala/app/PullRequestsController.scala +++ b/src/main/scala/app/PullRequestsController.scala @@ -18,16 +18,18 @@ import service.WebHookService._ import util.JGitUtil.DiffInfo import util.JGitUtil.CommitInfo -import model.{PullRequest, Issue} +import model.{PullRequest, Issue, CommitState} class PullRequestsController extends PullRequestsControllerBase with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService with CommitsService with ActivityService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator + with CommitStatusService trait PullRequestsControllerBase extends ControllerBase { self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService - with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator => + with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator + with CommitStatusService => private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase]) @@ -95,7 +97,6 @@ using(Git.open(getRepositoryDir(owner, name))){ git => val (commits, diffs) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo) - pulls.html.pullreq( issue, pullreq, (commits.flatten.map(commit => getCommitComments(owner, name, commit.id, true)).flatten.toList ::: getComments(owner, name, issueId)) @@ -159,9 +160,16 @@ val owner = repository.owner val name = repository.name getPullRequest(owner, name, issueId) map { case(issue, pullreq) => + val statuses = getCommitStatues(owner, name, pullreq.commitIdTo) + val hasConfrict = checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId) + val hasProblem = hasConfrict || (!statuses.isEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS) pulls.html.mergeguide( - checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId), + hasConfrict, + hasProblem, + issue, pullreq, + statuses, + repository, s"${context.baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git") } } getOrElse NotFound diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala index de0b140..3ec28e3 100644 --- a/src/main/scala/app/RepositoryViewerController.scala +++ b/src/main/scala/app/RepositoryViewerController.scala @@ -18,10 +18,11 @@ import org.eclipse.jgit.dircache.DirCache import org.eclipse.jgit.revwalk.RevCommit import service.WebHookService._ +import model.CommitState class RepositoryViewerController extends RepositoryViewerControllerBase with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService - with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService + with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService /** @@ -29,7 +30,7 @@ */ trait RepositoryViewerControllerBase extends ControllerBase { self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService - with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService => + with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService => ArchiveCommand.registerFormat("zip", new ZipFormat) ArchiveCommand.registerFormat("tar.gz", new TgzFormat) @@ -143,6 +144,56 @@ } }) + /** + * https://developer.github.com/v3/repos/statuses/#create-a-status + */ + post("/api/v3/repos/:owner/:repo/statuses/:sha")(collaboratorsOnly { repository => + (for{ + ref <- params.get("sha") + sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref) + data <- extractFromJsonBody[CreateAStatus] if data.isValid + creator <- context.loginAccount + state <- model.CommitState.valueOf(data.state) + statusId = createCommitStatus(repository.owner, repository.name, sha, data.context.getOrElse("default"), + state, data.target_url, data.description, new java.util.Date(), creator) + status <- getCommitStatus(repository.owner, repository.name, statusId) + } yield { + apiJson(WebHookCommitStatus(status, WebHookApiUser(creator))) + }) getOrElse NotFound + }) + + /** + * https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref + * + * ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name. + */ + get("/api/v3/repos/:owner/:repo/commits/:ref/statuses")(referrersOnly { repository => + (for{ + ref <- params.get("ref") + sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref) + } yield { + apiJson(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map{ case(status, creator) => + WebHookCommitStatus(status, WebHookApiUser(creator)) + }) + }) getOrElse NotFound + }) + + /** + * https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref + * + * ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name. + */ + get("/api/v3/repos/:owner/:repo/commits/:ref/status")(referrersOnly { repository => + (for{ + ref <- params.get("ref") + owner <- getAccountByUserName(repository.owner) + sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref) + } yield { + val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha) + apiJson(WebHookCombinedCommitStatus(sha, statuses, WebHookRepository(repository, owner))) + }) getOrElse NotFound + }) + get("/:owner/:repository/new/*")(collaboratorsOnly { repository => val (branch, path) = splitPath(repository, multiParams("splat").head) repo.html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList, diff --git a/src/main/scala/service/WebHookService.scala b/src/main/scala/service/WebHookService.scala index 1a91af9..25dae0b 100644 --- a/src/main/scala/service/WebHookService.scala +++ b/src/main/scala/service/WebHookService.scala @@ -2,7 +2,7 @@ import model.Profile._ import profile.simple._ -import model.{WebHook, Account, Issue, PullRequest, IssueComment, Repository} +import model.{WebHook, Account, Issue, PullRequest, IssueComment, Repository, CommitStatus, CommitState} import org.slf4j.LoggerFactory import service.RepositoryService.RepositoryInfo import util.JGitUtil @@ -168,7 +168,8 @@ { case x: Date => JString(parserISO.print(new DateTime(x).withZone(DateTimeZone.UTC))) } ) ) + FieldSerializer[WebHookApiUser]() + FieldSerializer[WebHookPullRequest]() + FieldSerializer[WebHookRepository]() + - FieldSerializer[WebHookCommitListItemParent]() + FieldSerializer[WebHookCommitListItem]() + FieldSerializer[WebHookCommitListItemCommit]() + FieldSerializer[WebHookCommitListItemParent]() + FieldSerializer[WebHookCommitListItem]() + FieldSerializer[WebHookCommitListItemCommit]() + + FieldSerializer[WebHookCommitStatus]() + FieldSerializer[WebHookCombinedCommitStatus]() } def apiPathSerializer(c:ApiContext) = new CustomSerializer[ApiPath](format => ( @@ -410,7 +411,7 @@ //val review_comments_url = ApiPath("${base.repo.url.path}/pulls/${number}/comments") //val review_comment_url = ApiPath("${base.repo.url.path}/pulls/comments/{number}") //val comments_url = ApiPath("${base.repo.url.path}/issues/${number}/comments") - //val statuses_url = ApiPath("${base.repo.url.path}/statuses/${head.sha}") + val statuses_url = ApiPath(s"${base.repo.url.path}/statuses/${head.sha}") } object WebHookPullRequest{ @@ -426,7 +427,7 @@ sha = pullRequest.commitIdFrom, ref = pullRequest.branch, repo = baseRepo), - mergeable = Some(true), // TODO: need check mergeable. + mergeable = None, // TODO: need check mergeable. title = issue.title, body = issue.content.getOrElse(""), user = user @@ -530,4 +531,75 @@ parents = commit.parents.map(WebHookCommitListItemParent(_)(repoFullName)))(repoFullName) } + /** + * https://developer.github.com/v3/repos/statuses/#create-a-status + */ + case class CreateAStatus( + /* state is Required. The state of the status. Can be one of pending, success, error, or failure. */ + state: String, + /* context is a string label to differentiate this status from the status of other systems. Default: "default" */ + context: Option[String], + /* The target URL to associate with this status. This URL will be linked from the GitHub UI to allow users to easily see the ‘source’ of the Status. */ + target_url: Option[String], + /* description is a short description of the status.*/ + description: Option[String] + ) { + def isValid: Boolean = { + CommitState.valueOf(state).isDefined && + target_url.filterNot(f => "\\Ahttps?://".r.findPrefixOf(f).isDefined && f.length<255).isEmpty && + context.filterNot(f => f.length<255).isEmpty && + description.filterNot(f => f.length<1000).isEmpty + } + } + + /** + * https://developer.github.com/v3/repos/statuses/#create-a-status + * https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref + */ + case class WebHookCommitStatus( + created_at: Date, + updated_at: Date, + state: String, + target_url: Option[String], + description: Option[String], + id: Int, + context: String, + creator: WebHookApiUser + )(sha: String, repoFullName: String) { + def url = ApiPath(s"/api/v3/repos/${repoFullName}/commits/${sha}/statuses") + } + + object WebHookCommitStatus { + def apply(status: CommitStatus, creator:WebHookApiUser): WebHookCommitStatus = WebHookCommitStatus( + created_at = status.registeredDate, + updated_at = status.updatedDate, + state = status.state.name, + target_url = status.targetUrl, + description= status.description, + id = status.commitStatusId, + context = status.context, + creator = creator + )(status.commitId, s"${status.userName}/${status.repositoryName}") + } + + /** + * https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref + */ + case class WebHookCombinedCommitStatus( + state: String, + sha: String, + total_count: Int, + statuses: Iterable[WebHookCommitStatus], + repository: WebHookRepository){ + // val commit_url = ApiPath(s"/api/v3/repos/${repository.full_name}/${sha}") + val url = ApiPath(s"/api/v3/repos/${repository.full_name}/commits/${sha}/status") + } + object WebHookCombinedCommitStatus { + def apply(sha:String, statuses: Iterable[(CommitStatus, Account)], repository:WebHookRepository): WebHookCombinedCommitStatus = WebHookCombinedCommitStatus( + state = CommitState.combine(statuses.map(_._1.state).toSet).name, + sha = sha, + total_count= statuses.size, + statuses = statuses.map{ case (s, a)=> WebHookCommitStatus(s, WebHookApiUser(a)) }, + repository = repository) + } } diff --git a/src/main/scala/util/JGitUtil.scala b/src/main/scala/util/JGitUtil.scala index 5c7be3f..47e6994 100644 --- a/src/main/scala/util/JGitUtil.scala +++ b/src/main/scala/util/JGitUtil.scala @@ -748,4 +748,17 @@ } } } + + /** + * Returns sha1 + * @param owner repository owner + * @param name repository name + * @param revstr A git object references expression + * @return sha1 + */ + def getShaByRef(owner:String, name:String,revstr: String): Option[String] = { + using(Git.open(getRepositoryDir(owner, name))){ git => + Option(git.getRepository.resolve(revstr)).map(ObjectId.toString(_)) + } + } } diff --git a/src/main/scala/view/helpers.scala b/src/main/scala/view/helpers.scala index caea648..1f2b406 100644 --- a/src/main/scala/view/helpers.scala +++ b/src/main/scala/view/helpers.scala @@ -4,6 +4,7 @@ import play.twirl.api.Html import util.StringUtil import service.RequestCache +import model.CommitState /** * Provides helper methods for Twirl templates. @@ -260,4 +261,17 @@ def mkHtml(separator: scala.xml.Elem) = Html(seq.mkString(separator.toString)) } + def commitStateIcon(state: CommitState) = Html(state match { + case CommitState.PENDING => "●" + case CommitState.SUCCESS => "✔" + case CommitState.ERROR => "×" + case CommitState.FAILURE => "×" + }) + + def commitStateText(state: CommitState, commitId:String) = state match { + case CommitState.PENDING => "Waiting to hear about "+commitId.substring(0,8) + case CommitState.SUCCESS => "All is well" + case CommitState.ERROR => "Failed" + case CommitState.FAILURE => "Failed" + } } diff --git a/src/main/twirl/pulls/conversation.scala.html b/src/main/twirl/pulls/conversation.scala.html index 33c923c..82a8982 100644 --- a/src/main/twirl/pulls/conversation.scala.html +++ b/src/main/twirl/pulls/conversation.scala.html @@ -8,7 +8,7 @@ hasWritePermission: Boolean, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) @import context._ -@import model.IssueComment +@import model.{IssueComment, CommitState} @import view.helpers._
@@ -20,25 +20,10 @@ case other => None }.exists(_.action == "merge")){ merged => @if(hasWritePermission && !issue.closed){ -
-
-
- -
- +