add protected-branch feature on pull-request page

* show commit-status if context is require status checks to pass.
  * disable merge if new commit-id has not `commit-status` ok on `Status-checkes`.
  * if some status includes required is not success, merge button is disabled.
  * if any required status is success, and some status not includes required, merge button is active, but button color is white.
  * if any required status is success, merge button is active, and button color is green.
This commit is contained in:
nazoking
2015-12-07 19:51:35 +09:00
parent 34240d16b5
commit 645af4d2c0
6 changed files with 193 additions and 120 deletions

View File

@@ -12,16 +12,26 @@ object ApiBranchProtection{
/** form for enabling-and-disabling-branch-protection */ /** form for enabling-and-disabling-branch-protection */
case class EnablingAndDisabling(protection: ApiBranchProtection) case class EnablingAndDisabling(protection: ApiBranchProtection)
def apply(info: Option[ProtectedBrancheService.ProtectedBranchInfo]): ApiBranchProtection = info match { def apply(info: ProtectedBrancheService.ProtectedBranchInfo): ApiBranchProtection = ApiBranchProtection(
case None => ApiBranchProtection(false, Some(statusNone)) enabled = info.enabled,
case Some(info) => ApiBranchProtection(true, Some(Status(if(info.includeAdministrators){ Everyone }else{ NonAdmins }, info.requireStatusChecksToPass))) required_status_checks = Some(Status(EnforcementLevel(info.enabled, info.includeAdministrators), info.contexts)))
}
val statusNone = Status(Off, Seq.empty) val statusNone = Status(Off, Seq.empty)
case class Status(enforcement_level: EnforcementLevel, contexts: Seq[String]) case class Status(enforcement_level: EnforcementLevel, contexts: Seq[String])
sealed class EnforcementLevel(val name: String) sealed class EnforcementLevel(val name: String)
case object Off extends EnforcementLevel("off") case object Off extends EnforcementLevel("off")
case object NonAdmins extends EnforcementLevel("non_admins") case object NonAdmins extends EnforcementLevel("non_admins")
case object Everyone extends EnforcementLevel("everyone") case object Everyone extends EnforcementLevel("everyone")
object EnforcementLevel {
def apply(enabled: Boolean, includeAdministrators: Boolean): EnforcementLevel = if(enabled){
if(includeAdministrators){
Everyone
}else{
NonAdmins
}
}else{
Off
}
}
implicit val enforcementLevelSerializer = new CustomSerializer[EnforcementLevel](format => ( implicit val enforcementLevelSerializer = new CustomSerializer[EnforcementLevel](format => (
{ {

View File

@@ -27,13 +27,13 @@ import scala.collection.JavaConverters._
class PullRequestsController extends PullRequestsControllerBase class PullRequestsController extends PullRequestsControllerBase
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
with CommitsService with ActivityService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator with CommitsService with ActivityService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
with CommitStatusService with MergeService with CommitStatusService with MergeService with ProtectedBrancheService
trait PullRequestsControllerBase extends ControllerBase { trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService 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 with MergeService => with CommitStatusService with MergeService with ProtectedBrancheService =>
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase]) private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
@@ -171,17 +171,21 @@ trait PullRequestsControllerBase extends ControllerBase {
val owner = repository.owner val owner = repository.owner
val name = repository.name val name = repository.name
getPullRequest(owner, name, issueId) map { case(issue, pullreq) => getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
val statuses = getCommitStatues(owner, name, pullreq.commitIdTo) val branchProtection = getProtectedBranchInfo(owner, name, pullreq.branch)
val statuses = branchProtection.withRequireStatues(getCommitStatues(owner, name, pullreq.commitIdTo))
val hasConfrict = LockUtil.lock(s"${owner}/${name}"){ val hasConfrict = LockUtil.lock(s"${owner}/${name}"){
checkConflict(owner, name, pullreq.branch, issueId) checkConflict(owner, name, pullreq.branch, issueId)
} }
val hasProblem = hasConfrict || (!statuses.isEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS) val hasRequiredStatusProblem = branchProtection.hasProblem(statuses, pullreq.commitIdTo, context.loginAccount.get.userName)
val hasProblem = hasRequiredStatusProblem || hasConfrict || (!statuses.isEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS)
html.mergeguide( html.mergeguide(
hasConfrict, hasConfrict,
hasProblem, hasProblem,
issue, issue,
pullreq, pullreq,
statuses, statuses,
branchProtection,
hasRequiredStatusProblem,
repository, repository,
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName, context.baseUrl).get) getRepository(pullreq.requestUserName, pullreq.requestRepositoryName, context.baseUrl).get)
} }

View File

@@ -1,6 +1,6 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.model.{Collaborator, Repository, Account, CommitState} import gitbucket.core.model.{Collaborator, Repository, Account, CommitState, CommitStatus}
import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile._
import gitbucket.core.util.JGitUtil import gitbucket.core.util.JGitUtil
import profile.simple._ import profile.simple._
@@ -16,14 +16,17 @@ object MockDB{
trait ProtectedBrancheService { trait ProtectedBrancheService {
import ProtectedBrancheService._ import ProtectedBrancheService._
def getProtectedBranchInfo(owner: String, repository: String, branch: String)(implicit session: Session): Option[ProtectedBranchInfo] = { private def getProtectedBranchInfoOpt(owner: String, repository: String, branch: String)(implicit session: Session): Option[ProtectedBranchInfo] = {
// TODO: mock // TODO: mock
MockDB.data.get((owner, repository, branch)).map{ case (includeAdministrators, requireStatusChecksToPass) => MockDB.data.get((owner, repository, branch)).map{ case (includeAdministrators, contexts) =>
new ProtectedBranchInfo(owner, repository, requireStatusChecksToPass, includeAdministrators) new ProtectedBranchInfo(owner, repository, true, contexts, includeAdministrators)
} }
} }
def getProtectedBranchInfo(owner: String, repository: String, branch: String)(implicit session: Session): ProtectedBranchInfo = {
getProtectedBranchInfoOpt(owner, repository, branch).getOrElse(ProtectedBranchInfo.disabled(owner, repository))
}
def isProtectedBranchNeedStatusCheck(owner: String, repository: String, branch: String, user: String)(implicit session: Session): Boolean = def isProtectedBranchNeedStatusCheck(owner: String, repository: String, branch: String, user: String)(implicit session: Session): Boolean =
getProtectedBranchInfo(owner, repository, branch).map{a => println(a); a.needStatusCheck(user)}.getOrElse(false) getProtectedBranchInfo(owner, repository, branch).needStatusCheck(user)
def getProtectedBranchList(owner: String, repository: String)(implicit session: Session): List[String] = { def getProtectedBranchList(owner: String, repository: String)(implicit session: Session): List[String] = {
// TODO: mock // TODO: mock
MockDB.data.filter{ MockDB.data.filter{
@@ -31,9 +34,9 @@ trait ProtectedBrancheService {
case _ => false case _ => false
}.map{ case ((_, _, branch), _) => branch }.toList }.map{ case ((_, _, branch), _) => branch }.toList
} }
def enableBranchProtection(owner: String, repository: String, branch:String, includeAdministrators: Boolean, requireStatusChecksToPass: Seq[String])(implicit session: Session): Unit = { def enableBranchProtection(owner: String, repository: String, branch:String, includeAdministrators: Boolean, contexts: Seq[String])(implicit session: Session): Unit = {
// TODO: mock // TODO: mock
MockDB.data.put((owner, repository, branch), includeAdministrators -> requireStatusChecksToPass) MockDB.data.put((owner, repository, branch), includeAdministrators -> contexts)
} }
def disableBranchProtection(owner: String, repository: String, branch:String)(implicit session: Session): Unit = { def disableBranchProtection(owner: String, repository: String, branch:String)(implicit session: Session): Unit = {
// TODO: mock // TODO: mock
@@ -43,7 +46,7 @@ trait ProtectedBrancheService {
def getBranchProtectedReason(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)(implicit session: Session): Option[String] = { def getBranchProtectedReason(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)(implicit session: Session): Option[String] = {
val branch = command.getRefName.stripPrefix("refs/heads/") val branch = command.getRefName.stripPrefix("refs/heads/")
if(branch != command.getRefName){ if(branch != command.getRefName){
getProtectedBranchInfo(owner, repository, branch).flatMap(_.getStopReason(receivePack, command, pusher)) getProtectedBranchInfo(owner, repository, branch).getStopReason(receivePack, command, pusher)
}else{ }else{
None None
} }
@@ -53,13 +56,14 @@ object ProtectedBrancheService {
case class ProtectedBranchInfo( case class ProtectedBranchInfo(
owner: String, owner: String,
repository: String, repository: String,
enabled: Boolean,
/** /**
* Require status checks to pass before merging * Require status checks to pass before merging
* Choose which status checks must pass before branches can be merged into test. * Choose which status checks must pass before branches can be merged into test.
* When enabled, commits must first be pushed to another branch, * When enabled, commits must first be pushed to another branch,
* then merged or pushed directly to test after status checks have passed. * then merged or pushed directly to test after status checks have passed.
*/ */
requireStatusChecksToPass: Seq[String], contexts: Seq[String],
/** /**
* Include administrators * Include administrators
* Enforce required status checks for repository administrators. * Enforce required status checks for repository administrators.
@@ -74,32 +78,56 @@ object ProtectedBrancheService {
* Can't have changes merged into them until required status checks pass * Can't have changes merged into them until required status checks pass
*/ */
def getStopReason(receivePack: ReceivePack, command: ReceiveCommand, pusher: String)(implicit session: Session): Option[String] = { def getStopReason(receivePack: ReceivePack, command: ReceiveCommand, pusher: String)(implicit session: Session): Option[String] = {
command.getType() match { if(enabled){
case ReceiveCommand.Type.UPDATE|ReceiveCommand.Type.UPDATE_NONFASTFORWARD if receivePack.isAllowNonFastForwards => command.getType() match {
Some("Cannot force-push to a protected branch") case ReceiveCommand.Type.UPDATE|ReceiveCommand.Type.UPDATE_NONFASTFORWARD if receivePack.isAllowNonFastForwards =>
case ReceiveCommand.Type.UPDATE|ReceiveCommand.Type.UPDATE_NONFASTFORWARD if needStatusCheck(pusher) => Some("Cannot force-push to a protected branch")
unSuccessedContexts(command.getNewId.name) match { case ReceiveCommand.Type.UPDATE|ReceiveCommand.Type.UPDATE_NONFASTFORWARD if needStatusCheck(pusher) =>
case s if s.size == 1 => Some(s"""Required status check "${s.toSeq(0)}" is expected""") unSuccessedContexts(command.getNewId.name) match {
case s if s.size >= 1 => Some(s"${s.size} of ${requireStatusChecksToPass.size} required status checks are expected") case s if s.size == 1 => Some(s"""Required status check "${s.toSeq(0)}" is expected""")
case _ => None case s if s.size >= 1 => Some(s"${s.size} of ${contexts.size} required status checks are expected")
} case _ => None
case ReceiveCommand.Type.DELETE => }
Some("Cannot delete a protected branch") case ReceiveCommand.Type.DELETE =>
case _ => None Some("Cannot delete a protected branch")
case _ => None
}
}else{
None
} }
} }
def unSuccessedContexts(sha1: String)(implicit session: Session): Set[String] = if(requireStatusChecksToPass.isEmpty){ def unSuccessedContexts(sha1: String)(implicit session: Session): Set[String] = if(contexts.isEmpty){
Set.empty Set.empty
} else { } else {
requireStatusChecksToPass.toSet -- getCommitStatues(owner, repository, sha1).filter(_.state == CommitState.SUCCESS).map(_.context).toSet contexts.toSet -- getCommitStatues(owner, repository, sha1).filter(_.state == CommitState.SUCCESS).map(_.context).toSet
} }
def needStatusCheck(pusher: String)(implicit session: Session): Boolean = def needStatusCheck(pusher: String)(implicit session: Session): Boolean =
if(requireStatusChecksToPass.isEmpty){ if(!enabled || contexts.isEmpty){
false false
}else if(includeAdministrators){ }else if(includeAdministrators){
true true
}else{ }else{
!isAdministrator(pusher) !isAdministrator(pusher)
} }
def withRequireStatues(statuses: List[CommitStatus]): List[CommitStatus] = {
statuses ++ (contexts.toSet -- statuses.map(_.context).toSet).map{ context => CommitStatus(
commitStatusId = 0,
userName = owner,
repositoryName = repository,
commitId = "",
context = context,
state = CommitState.PENDING,
targetUrl = None,
description = Some("Waiting for status to be reported"),
creator = "",
registeredDate = new java.util.Date(),
updatedDate = new java.util.Date())
}
}
def hasProblem(statuses: List[CommitStatus], sha1: String, account: String)(implicit session: Session): Boolean =
needStatusCheck(account) && contexts.exists(context => statuses.find(_.context == context).map(_.state) != Some(CommitState.SUCCESS))
}
object ProtectedBranchInfo{
def disabled(owner: String, repository: String): ProtectedBranchInfo = ProtectedBranchInfo(owner, repository, false, Nil, false)
} }
} }

View File

@@ -288,10 +288,10 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
} }
def commitStateIcon(state: CommitState) = Html(state match { def commitStateIcon(state: CommitState) = Html(state match {
case CommitState.PENDING => "" case CommitState.PENDING => """<i style="color:inherit;width:inherit;height:inherit" class="octicon octicon-primitive-dot"></i>"""
case CommitState.SUCCESS => "&#x2714;" case CommitState.SUCCESS => """<i style="color:inherit;width:inherit;height:inherit" class="octicon octicon-check"></i>"""
case CommitState.ERROR => "×" case CommitState.ERROR => """<i style="color:inherit;width:inherit;height:inherit" class="octicon octicon-x"></i>"""
case CommitState.FAILURE => "×" case CommitState.FAILURE => """<i style="color:inherit;width:inherit;height:inherit" class="octicon octicon-x"></i>"""
}) })
def commitStateText(state: CommitState, commitId:String) = state match { def commitStateText(state: CommitState, commitId:String) = state match {

View File

@@ -3,6 +3,8 @@
issue: gitbucket.core.model.Issue, issue: gitbucket.core.model.Issue,
pullreq: gitbucket.core.model.PullRequest, pullreq: gitbucket.core.model.PullRequest,
statuses: List[model.CommitStatus], statuses: List[model.CommitStatus],
branchProtection: gitbucket.core.service.ProtectedBrancheService.ProtectedBranchInfo,
hasRequiredStatusProblem: Boolean,
originRepository: gitbucket.core.service.RepositoryService.RepositoryInfo, originRepository: gitbucket.core.service.RepositoryService.RepositoryInfo,
forkedRepository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) forkedRepository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.service.SystemSettingsService @import gitbucket.core.service.SystemSettingsService
@@ -14,35 +16,27 @@
<div id="merge-pull-request"> <div id="merge-pull-request">
@if(!statuses.isEmpty){ @if(!statuses.isEmpty){
<div class="build-statuses"> <div class="build-statuses">
@if(statuses.size==1){ @defining(statuses.groupBy(_.state)){ stateMap =>
@defining(statuses.head){ status => @defining(CommitState.combine(stateMap.keySet)){ state =>
<div class="build-status-item"> <div class="build-status-item-header">
@status.targetUrl.map{ url => <a class="pull-right" href="@url">Details</a> } <a class="pull-right" id="toggle-all-checks"></a>
<span class="build-status-icon text-@{status.state.name}">@commitStateIcon(status.state)</span> <span class="build-status-icon text-@{state.name}">@commitStateIcon(state)</span>
<strong class="text-@{status.state.name}">@commitStateText(status.state, pullreq.commitIdTo)</strong> <strong class="text-@{state.name}">@commitStateText(state, pullreq.commitIdTo)</strong>
@status.description.map{ desc => <span class="muted">— @desc</span> } <span class="text-@{state.name}">— @{stateMap.map{ case (keyState, states) => states.size+" "+keyState.name }.mkString(", ")} checks</span>
</div> </div>
} <div class="build-statuses-list" style="@if(state==CommitState.SUCCESS){ display:none; }else{ }">
} else { @statuses.map{ status =>
@defining(statuses.groupBy(_.state)){ stateMap =>
@defining(CommitState.combine(stateMap.keySet)){ state =>
<div class="build-status-item"> <div class="build-status-item">
<a class="pull-right" id="toggle-all-checks"></a> <div class="pull-right">
<span class="build-status-icon text-@{state.name}">@commitStateIcon(state)</span> @branchProtection.contexts.find(_==status.context).map{ url => <span class="label">Required</span> }
<strong class="text-@{state.name}">@commitStateText(state, pullreq.commitIdTo)</strong> @status.targetUrl.map{ url => <a href="@url">Details</a> }
<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>
} <span class="build-status-icon text-@{status.state.name}">@commitStateIcon(status.state)</span>
<strong>@status.context</strong>
@status.description.map{ desc => <span class="muted">— @desc</span> }
</div> </div>
} }
</div>
} }
} }
</div> </div>
@@ -52,67 +46,74 @@
</div> </div>
<div> <div>
@if(hasConflict){ @if(hasConflict){
<span class="strong">We cant automatically merge this pull request.</span> <div class="merge-indicator merge-indicator-alert"><span class="octicon octicon-alert"></span></div>
<span class="strong">This branch has conflicts that must be resolved</span>
<div class="small">
<a href="#" class="show-command-line">Use the command line</a> to resolve conflicts before continuing.
</div>
} else { @if(hasRequiredStatusProblem) {
<div class="merge-indicator merge-indicator-warning"><span class="octicon octicon-primitive-dot"></span></div>
<span class="strong">Required statuses must pass before merging.</span>
<div class="small">
All required status checks on this pull request must run successfully to enable automatic merging.
</div>
} else { } else {
@if(hasProblem){ <div class="merge-indicator merge-indicator-success"><span class="octicon octicon-check"></span></div>
<span class="strong">Merge with caution!</span> <span class="strong">Merging can be performed automatically.</span>
<div class="small">
Merging can be performed automatically.
</div>
} }
</div>
<div style="padding:15px;border-top:solid 1px #e5e5e5;background:#fafafa">
<input type="button" class="btn @if(!hasProblem){ btn-success }" id="merge-pull-request-button" value="Merge pull request"@if(hasConflict||hasRequiredStatusProblem){ disabled="true"}/>
You can also merge branches on the <a href="#" class="show-command-line">command line</a>.
<div id="command-line" style="display: none;margin-top: 15px;">
<hr>
@if(hasConflict){
<span class="strong">Checkout via command line</span>
<p>
If you cannot merge a pull request automatically here, you have the option of checking
it out via command line to resolve conflicts and perform a manual merge.
</p>
} else { } else {
<span class="strong">This pull request can be automatically merged.</span> <span class="strong">Merging via command line</span>
<p>
If you do not want to use the merge button or an automatic merge cannot be performed,
you can perform a manual merge on the command line.
</p>
} }
} @helper.html.copy("repository-url-copy", forkedRepository.httpUrl, true){
</div> <div class="btn-group" data-toggle="buttons-radio">
<div class="small"> <button class="btn btn-small active" type="button" id="repository-url-http">HTTP</button>
@if(hasConflict){ @if(settings.ssh && loginAccount.isDefined){
<a href="#" id="show-command-line">Use the command line</a> to resolve conflicts before continuing. <button class="btn btn-small" type="button" id="repository-url-ssh" style="border-radius: 0px;">SSH</button>
} else { }
You can also merge branches on the <a href="#" id="show-command-line">command line</a>. </div>
} <input type="text" style="width: 500px;" value="@forkedRepository.httpUrl" id="repository-url" readonly>
</div> }
<div id="command-line" style="display: none;"> <div>
<hr> <p>
@if(hasConflict){ <span class="strong">Step 1:</span> From your project repository, check out a new branch and test the changes.
<span class="strong">Checkout via command line</span> </p>
<p> @defining(s"git checkout -b ${pullreq.requestUserName}-${pullreq.requestBranch} ${pullreq.branch}\n" +
If you cannot merge a pull request automatically here, you have the option of checking s"git pull ${forkedRepository.httpUrl} ${pullreq.requestBranch}"){ command =>
it out via command line to resolve conflicts and perform a manual merge. @helper.html.copy("merge-command-copy-1", command){
</p> <pre style="width: 600px; float: left; font-size: 12px; border-radius: 3px 0px 3px 3px;" id="merge-command">@Html(command)</pre>
} else { }
<span class="strong">Merging via command line</span>
<p>
If you do not want to use the merge button or an automatic merge cannot be performed,
you can perform a manual merge on the command line.
</p>
}
@helper.html.copy("repository-url-copy", forkedRepository.httpUrl, true){
<div class="btn-group" data-toggle="buttons-radio">
<button class="btn btn-small active" type="button" id="repository-url-http">HTTP</button>
@if(settings.ssh && loginAccount.isDefined){
<button class="btn btn-small" type="button" id="repository-url-ssh" style="border-radius: 0px;">SSH</button>
} }
</div> </div>
<input type="text" style="width: 500px;" value="@forkedRepository.httpUrl" id="repository-url" readonly> <div>
} <p>
<div> <span class="strong">Step 2:</span> Merge the changes and update on the server.
<p> </p>
<span class="strong">Step 1:</span> From your project repository, check out a new branch and test the changes. @defining(s"git checkout ${pullreq.branch}\ngit merge --no-ff ${pullreq.requestUserName}-${pullreq.requestBranch}\n" +
</p> s"git push origin ${pullreq.branch}"){ command =>
@defining(s"git checkout -b ${pullreq.requestUserName}-${pullreq.requestBranch} ${pullreq.branch}\n" + @helper.html.copy("merge-command-copy-2", command){
s"git pull ${forkedRepository.httpUrl} ${pullreq.requestBranch}"){ command => <pre style="width: 600px; float: left; font-size: 12px; border-radius: 3px 0px 3px 3px;">@command</pre>
@helper.html.copy("merge-command-copy-1", command){ }
<pre style="width: 600px; float: left; font-size: 12px; border-radius: 3px 0px 3px 3px;" id="merge-command">@Html(command)</pre>
} }
} </div>
</div>
<div>
<p>
<span class="strong">Step 2:</span> Merge the changes and update on the server.
</p>
@defining(s"git checkout ${pullreq.branch}\ngit merge --no-ff ${pullreq.requestUserName}-${pullreq.requestBranch}\n" +
s"git push origin ${pullreq.branch}"){ command =>
@helper.html.copy("merge-command-copy-2", command){
<pre style="width: 600px; float: left; font-size: 12px; border-radius: 3px 0px 3px 3px;">@command</pre>
}
}
</div> </div>
</div> </div>
</div> </div>
@@ -134,8 +135,8 @@
<script> <script>
$(function(){ $(function(){
$('#show-command-line').click(function(){ $('.show-command-line').click(function(){
$('#command-line').show(); $('#command-line').toggle();
return false; return false;
}); });
function setToggleAllChecksLabel(){ function setToggleAllChecksLabel(){

View File

@@ -1405,13 +1405,43 @@ div.author-info div.committer {
margin: -10px -10px 10px -10px; margin: -10px -10px 10px -10px;
} }
.build-statuses .build-status-item{ .build-statuses .build-status-item{
padding: 10px 15px 10px 12px; padding: 10px 15px 10px 64px;
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
} }
.build-statuses-list .build-status-item{ .build-statuses-list .build-status-item{
background-color: #fafafa; background-color: #fafafa;
} }
.merge-indicator{
float:left;
border-radius: 50%;
width: 30px;
height: 30px;
line-height: 30px;
text-align: center;
vertical-align: middle;
margin-right: 10px;
}
.merge-indicator-success{
background-color: #6cc644;
}
.merge-indicator-warning{
background-color: #cea61b;
}
.merge-indicator-alert{
background-color: #888;
}
.merge-indicator .octicon{
color: white;
font-size: 16px;
width: 15px;
height: 15px;
}
.merge-indicator-warning .octicon{
color: white;
font-size: 30px;
}
/****************************************************************************/ /****************************************************************************/
/* Diff */ /* Diff */
/****************************************************************************/ /****************************************************************************/