mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-01-07 08:02:14 +01:00
add CommitStatus api and views.
This commit is contained in:
@@ -18,16 +18,18 @@ import org.eclipse.jgit.errors.NoMergeBaseException
|
||||
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 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
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 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
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
|
||||
|
||||
@@ -18,10 +18,11 @@ import jp.sf.amateras.scalatra.forms._
|
||||
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 @@ class RepositoryViewerController extends RepositoryViewerControllerBase
|
||||
*/
|
||||
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 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
||||
@@ -2,7 +2,7 @@ package service
|
||||
|
||||
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 @@ object WebHookService {
|
||||
{ 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 @@ object WebHookService {
|
||||
//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 @@ object WebHookService {
|
||||
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 @@ object WebHookService {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -748,4 +748,17 @@ object JGitUtil {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(_))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.text.SimpleDateFormat
|
||||
import play.twirl.api.Html
|
||||
import util.StringUtil
|
||||
import service.RequestCache
|
||||
import model.CommitState
|
||||
|
||||
/**
|
||||
* Provides helper methods for Twirl templates.
|
||||
@@ -260,4 +261,17 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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._
|
||||
<div class="row-fluid">
|
||||
<div class="span10">
|
||||
@@ -20,25 +20,10 @@
|
||||
case other => None
|
||||
}.exists(_.action == "merge")){ merged =>
|
||||
@if(hasWritePermission && !issue.closed){
|
||||
<div class="box issue-comment-box" style="background-color: #d8f5cd;">
|
||||
<div class="box-content"class="issue-content" style="border: 1px solid #95c97e; padding: 10px;">
|
||||
<div id="merge-pull-request">
|
||||
<div class="check-conflict" style="display: none;">
|
||||
<img src="@assets/common/images/indicator.gif"/> Checking...
|
||||
</div>
|
||||
</div>
|
||||
<div id="confirm-merge-form" style="display: none;">
|
||||
<form method="POST" action="@url(repository)/pull/@issue.issueId/merge">
|
||||
<div class="strong">
|
||||
Merge pull request #@issue.issueId from @{pullreq.requestUserName}/@{pullreq.requestBranch}
|
||||
</div>
|
||||
<span id="error-message" class="error"></span>
|
||||
<textarea name="message" style="width: 635px; height: 80px;">@issue.title</textarea>
|
||||
<div>
|
||||
<input type="button" class="btn" value="Cancel" id="cancel-merge-pull-request"/>
|
||||
<input type="submit" class="btn btn-success" value="Confirm merge"/>
|
||||
</div>
|
||||
</form>
|
||||
<div class="check-conflict" style="display: none;">
|
||||
<div class="box issue-comment-box" style="background-color: #fbeed5">
|
||||
<div class="box-content"class="issue-content" style="border: 1px solid #c09853; padding: 10px;">
|
||||
<img src="@assets/common/images/indicator.gif"/> Checking...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,61 @@
|
||||
@(hasConflict: Boolean,
|
||||
hasProblem: Boolean,
|
||||
issue: model.Issue,
|
||||
pullreq: model.PullRequest,
|
||||
statuses: List[model.CommitStatus],
|
||||
repository: service.RepositoryService.RepositoryInfo,
|
||||
requestRepositoryUrl: String)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@import model.CommitState
|
||||
<div class="box issue-comment-box" style="background-color: @if(hasProblem){ #fbeed5 }else{ #d8f5cd };">
|
||||
<div class="box-content"class="issue-content" style="border: 1px solid @if(hasProblem){ #c09853 }else{ #95c97e }; padding: 10px;">
|
||||
<div id="merge-pull-request">
|
||||
@if(!statuses.isEmpty){
|
||||
<div class="build-statuses">
|
||||
@if(statuses.size==1){
|
||||
@defining(statuses.head){ status =>
|
||||
<div class="build-status-item">
|
||||
@status.targetUrl.map{ url => <a class="pull-right" href="@url">Details</a> }
|
||||
<span class="build-status-icon text-@{status.state.name}">@commitStateIcon(status.state)</span>
|
||||
<strong class="text-@{status.state.name}">@commitStateText(status.state, pullreq.commitIdTo)</strong>
|
||||
@status.description.map{ desc => <span class="muted">— @desc</span> }
|
||||
</div>
|
||||
}
|
||||
}else{
|
||||
@defining(statuses.groupBy(_.state)){ stateMap => @defining(CommitState.combine(stateMap.keySet)){ state =>
|
||||
<div class="build-status-item">
|
||||
<a class="pull-right" id="toggle-all-checks"></a>
|
||||
<span class="build-status-icon text-@{state.name}">@commitStateIcon(state)</span>
|
||||
<strong class="text-@{state.name}">@commitStateText(state, pullreq.commitIdTo)</strong>
|
||||
<span class="text-@{state.name}">— @{stateMap.map{ case (keyState, states) => states.size+" "+keyState.name }.mkString(", ")} checks</span>
|
||||
</div>
|
||||
<div class="build-statuses-list" style="@if(state==CommitState.SUCCESS){ display:none; }else{ }">
|
||||
@statuses.map{ status =>
|
||||
<div class="build-status-item">
|
||||
@status.targetUrl.map{ url => <a class="pull-right" href="@url">Details</a> }
|
||||
<span class="build-status-icon text-@{status.state.name}">@commitStateIcon(status.state)</span>
|
||||
<span class="text-@{status.state.name}">@status.context</span>
|
||||
@status.description.map{ desc => <span class="muted">— @desc</span> }
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} }
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="pull-right">
|
||||
<input type="button" class="btn btn-success" id="merge-pull-request-button" value="Merge pull request"@if(hasConflict){ disabled="true"}/>
|
||||
<input type="button" class="btn @if(!hasProblem){ btn-success }" id="merge-pull-request-button" value="Merge pull request"@if(hasConflict){ disabled="true"}/>
|
||||
</div>
|
||||
<div>
|
||||
@if(hasConflict){
|
||||
<span class="strong">We can’t automatically merge this pull request.</span>
|
||||
} else{ @if(hasProblem){
|
||||
<span class="strong">Merge with caution!</span>
|
||||
} else {
|
||||
<span class="strong">This pull request can be automatically merged.</span>
|
||||
}
|
||||
} }
|
||||
</div>
|
||||
<div class="small">
|
||||
@if(hasConflict){
|
||||
@@ -69,12 +113,38 @@
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div id="confirm-merge-form" style="display: none;">
|
||||
<form method="POST" action="@url(repository)/pull/@pullreq.issueId/merge">
|
||||
<div class="strong">
|
||||
Merge pull request #@issue.issueId from @{pullreq.requestUserName}/@{pullreq.requestBranch}
|
||||
</div>
|
||||
<span id="error-message" class="error"></span>
|
||||
<textarea name="message" style="width: 635px; height: 80px;">@issue.title</textarea>
|
||||
<div>
|
||||
<input type="button" class="btn" value="Cancel" id="cancel-merge-pull-request"/>
|
||||
<input type="submit" class="btn btn-success" value="Confirm merge"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
$('#show-command-line').click(function(){
|
||||
$('#command-line').show();
|
||||
return false;
|
||||
});
|
||||
function setToggleAllChecksLabel(){
|
||||
$("#toggle-all-checks").text($('.build-statuses-list').is(":visible") ? "Hide all checks" : "Show all checks");
|
||||
}
|
||||
setToggleAllChecksLabel();
|
||||
$('#toggle-all-checks').click(function(){
|
||||
$('.build-statuses-list').toggle();
|
||||
setToggleAllChecksLabel();
|
||||
})
|
||||
|
||||
$('#merge-pull-request-button').click(function(){
|
||||
$('#merge-pull-request').hide();
|
||||
|
||||
@@ -1006,6 +1006,24 @@ div.author-info div.committer {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.text-pending{
|
||||
color: #cea61b;
|
||||
}
|
||||
.text-failure{
|
||||
color: #bd2c00;
|
||||
}
|
||||
|
||||
.box-content .build-statuses{
|
||||
margin: -10px -10px 10px -10px;
|
||||
}
|
||||
.build-statuses .build-status-item{
|
||||
padding: 10px 15px 10px 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.build-statuses-list .build-status-item{
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
/****************************************************************************/
|
||||
/* Diff */
|
||||
/****************************************************************************/
|
||||
|
||||
Reference in New Issue
Block a user