Merge remote-tracking branch 'origin/api-support'

This commit is contained in:
Tomofumi Tanaka
2015-03-24 23:40:01 +09:00
63 changed files with 2824 additions and 440 deletions

View File

@@ -70,6 +70,9 @@ object MyBuild extends Build {
EclipseKeys.withSource := true, EclipseKeys.withSource := true,
javacOptions in compile ++= Seq("-target", "7", "-source", "7"), javacOptions in compile ++= Seq("-target", "7", "-source", "7"),
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"), testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),
javaOptions in Test += "-Dgitbucket.home=target/gitbucket_home_for_test",
testOptions in Test += Tests.Setup( () => new java.io.File("target/gitbucket_home_for_test").mkdir() ),
fork in Test := true,
packageOptions += Package.MainClass("JettyLauncher") packageOptions += Package.MainClass("JettyLauncher")
).enablePlugins(SbtTwirl) ).enablePlugins(SbtTwirl)
} }

View File

@@ -0,0 +1,42 @@
DROP TABLE IF EXISTS ACCESS_TOKEN;
CREATE TABLE ACCESS_TOKEN (
ACCESS_TOKEN_ID INT NOT NULL AUTO_INCREMENT,
TOKEN_HASH VARCHAR(40) NOT NULL,
USER_NAME VARCHAR(100) NOT NULL,
NOTE TEXT NOT NULL
);
ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_PK PRIMARY KEY (ACCESS_TOKEN_ID);
ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME)
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_TOKEN_HASH UNIQUE(TOKEN_HASH);
DROP TABLE IF EXISTS COMMIT_STATUS;
CREATE TABLE COMMIT_STATUS(
COMMIT_STATUS_ID INT AUTO_INCREMENT,
USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL,
COMMIT_ID VARCHAR(40) NOT NULL,
CONTEXT VARCHAR(255) NOT NULL, -- context is too long (maximum is 255 characters)
STATE VARCHAR(10) NOT NULL, -- pending, success, error, or failure
TARGET_URL VARCHAR(200),
DESCRIPTION TEXT,
CREATOR VARCHAR(100) NOT NULL,
REGISTERED_DATE TIMESTAMP NOT NULL, -- CREATED_AT
UPDATED_DATE TIMESTAMP NOT NULL -- UPDATED_AT
);
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_PK PRIMARY KEY (COMMIT_STATUS_ID);
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_1
UNIQUE (USER_NAME, REPOSITORY_NAME, COMMIT_ID, CONTEXT);
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_FK1
FOREIGN KEY (USER_NAME, REPOSITORY_NAME)
REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME)
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_FK2
FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME)
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_FK3
FOREIGN KEY (CREATOR) REFERENCES ACCOUNT (USER_NAME)
ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -1,13 +1,14 @@
import gitbucket.core.controller._ import gitbucket.core.controller._
import gitbucket.core.plugin.PluginRegistry import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.servlet.{TransactionFilter, BasicAuthenticationFilter} import gitbucket.core.servlet.{AccessTokenAuthenticationFilter, BasicAuthenticationFilter, TransactionFilter}
import gitbucket.core.util.Directory import gitbucket.core.util.Directory
//import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
import org.scalatra._
import javax.servlet._
import java.util.EnumSet import java.util.EnumSet
import javax.servlet._
import org.scalatra._
class ScalatraBootstrap extends LifeCycle { class ScalatraBootstrap extends LifeCycle {
override def init(context: ServletContext) { override def init(context: ServletContext) {
@@ -16,7 +17,8 @@ class ScalatraBootstrap extends LifeCycle {
context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*") context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter) context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter)
context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*") context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
context.addFilter("accessTokenAuthenticationFilter", new AccessTokenAuthenticationFilter)
context.getFilterRegistration("accessTokenAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/api/v3/*")
// Register controllers // Register controllers
context.mount(new AnonymousAccessController, "/*") context.mount(new AnonymousAccessController, "/*")

View File

@@ -0,0 +1,25 @@
package gitbucket.core.api
import gitbucket.core.model.{Account, CommitState, CommitStatus}
/**
* https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
*/
case class ApiCombinedCommitStatus(
state: String,
sha: String,
total_count: Int,
statuses: Iterable[ApiCommitStatus],
repository: ApiRepository){
// 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 ApiCombinedCommitStatus {
def apply(sha:String, statuses: Iterable[(CommitStatus, Account)], repository:ApiRepository): ApiCombinedCommitStatus = ApiCombinedCommitStatus(
state = CommitState.combine(statuses.map(_._1.state).toSet).name,
sha = sha,
total_count= statuses.size,
statuses = statuses.map{ case (s, a)=> ApiCommitStatus(s, ApiUser(a)) },
repository = repository)
}

View File

@@ -0,0 +1,26 @@
package gitbucket.core.api
import gitbucket.core.model.IssueComment
import java.util.Date
/**
* https://developer.github.com/v3/issues/comments/
*/
case class ApiComment(
id: Int,
user: ApiUser,
body: String,
created_at: Date,
updated_at: Date)
object ApiComment{
def apply(comment: IssueComment, user: ApiUser): ApiComment =
ApiComment(
id = comment.commentId,
user = user,
body = comment.content,
created_at = comment.registeredDate,
updated_at = comment.updatedDate)
}

View File

@@ -0,0 +1,48 @@
package gitbucket.core.api
import gitbucket.core.util.JGitUtil
import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util.RepositoryName
import org.eclipse.jgit.diff.DiffEntry
import org.eclipse.jgit.api.Git
import java.util.Date
/**
* https://developer.github.com/v3/repos/commits/
*/
case class ApiCommit(
id: String,
message: String,
timestamp: Date,
added: List[String],
removed: List[String],
modified: List[String],
author: ApiPersonIdent,
committer: ApiPersonIdent)(repositoryName:RepositoryName){
val url = ApiPath(s"/api/v3/${repositoryName.fullName}/commits/${id}")
val html_url = ApiPath(s"/${repositoryName.fullName}/commit/${id}")
}
object ApiCommit{
def apply(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiCommit = {
val diffs = JGitUtil.getDiffs(git, commit.id, false)
ApiCommit(
id = commit.id,
message = commit.fullMessage,
timestamp = commit.commitTime,
added = diffs._1.collect {
case x if x.changeType == DiffEntry.ChangeType.ADD => x.newPath
},
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 = ApiPersonIdent.author(commit),
committer = ApiPersonIdent.committer(commit)
)(repositoryName)
}
}

View File

@@ -0,0 +1,42 @@
package gitbucket.core.api
import gitbucket.core.api.ApiCommitListItem._
import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util.RepositoryName
/**
* https://developer.github.com/v3/repos/commits/
*/
case class ApiCommitListItem(
sha: String,
commit: Commit,
author: Option[ApiUser],
committer: Option[ApiUser],
parents: Seq[Parent])(repositoryName: RepositoryName) {
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${sha}")
}
object ApiCommitListItem {
def apply(commit: CommitInfo, repositoryName: RepositoryName): ApiCommitListItem = ApiCommitListItem(
sha = commit.id,
commit = Commit(
message = commit.fullMessage,
author = ApiPersonIdent.author(commit),
committer = ApiPersonIdent.committer(commit)
)(commit.id, repositoryName),
author = None,
committer = None,
parents = commit.parents.map(Parent(_)(repositoryName)))(repositoryName)
case class Parent(sha: String)(repositoryName: RepositoryName){
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${sha}")
}
case class Commit(
message: String,
author: ApiPersonIdent,
committer: ApiPersonIdent)(sha:String, repositoryName: RepositoryName) {
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/commits/${sha}")
}
}

View File

@@ -0,0 +1,38 @@
package gitbucket.core.api
import gitbucket.core.model.CommitStatus
import gitbucket.core.util.RepositoryName
import java.util.Date
/**
* 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 ApiCommitStatus(
created_at: Date,
updated_at: Date,
state: String,
target_url: Option[String],
description: Option[String],
id: Int,
context: String,
creator: ApiUser
)(sha: String, repositoryName: RepositoryName) {
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${sha}/statuses")
}
object ApiCommitStatus {
def apply(status: CommitStatus, creator:ApiUser): ApiCommitStatus = ApiCommitStatus(
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, RepositoryName(status))
}

View File

@@ -0,0 +1,5 @@
package gitbucket.core.api
case class ApiError(
message: String,
documentation_url: Option[String] = None)

View File

@@ -0,0 +1,31 @@
package gitbucket.core.api
import gitbucket.core.model.Issue
import java.util.Date
/**
* https://developer.github.com/v3/issues/
*/
case class ApiIssue(
number: Int,
title: String,
user: ApiUser,
// labels,
state: String,
created_at: Date,
updated_at: Date,
body: String)
object ApiIssue{
def apply(issue: Issue, user: ApiUser): ApiIssue =
ApiIssue(
number = issue.issueId,
title = issue.title,
user = user,
state = if(issue.closed){ "closed" }else{ "open" },
body = issue.content.getOrElse(""),
created_at = issue.registeredDate,
updated_at = issue.updatedDate)
}

View File

@@ -0,0 +1,6 @@
package gitbucket.core.api
/**
* path for api url. if set path '/repos/aa/bb' then, expand 'http://server:post/repos/aa/bb' when converted to json.
*/
case class ApiPath(path: String)

View File

@@ -0,0 +1,25 @@
package gitbucket.core.api
import gitbucket.core.util.JGitUtil.CommitInfo
import java.util.Date
case class ApiPersonIdent(
name: String,
email: String,
date: Date)
object ApiPersonIdent {
def author(commit: CommitInfo): ApiPersonIdent =
ApiPersonIdent(
name = commit.authorName,
email = commit.authorEmailAddress,
date = commit.authorTime)
def committer(commit: CommitInfo): ApiPersonIdent =
ApiPersonIdent(
name = commit.committerName,
email = commit.committerEmailAddress,
date = commit.commitTime)
}

View File

@@ -0,0 +1,59 @@
package gitbucket.core.api
import gitbucket.core.model.{Issue, PullRequest}
import java.util.Date
/**
* https://developer.github.com/v3/pulls/
*/
case class ApiPullRequest(
number: Int,
updated_at: Date,
created_at: Date,
head: ApiPullRequest.Commit,
base: ApiPullRequest.Commit,
mergeable: Option[Boolean],
title: String,
body: String,
user: ApiUser) {
val html_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}")
//val diff_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.diff")
//val patch_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.patch")
val url = ApiPath(s"${base.repo.url.path}/pulls/${number}")
//val issue_url = ApiPath(s"${base.repo.url.path}/issues/${number}")
val commits_url = ApiPath(s"${base.repo.url.path}/pulls/${number}/commits")
val review_comments_url = ApiPath(s"${base.repo.url.path}/pulls/${number}/comments")
val review_comment_url = ApiPath(s"${base.repo.url.path}/pulls/comments/{number}")
val comments_url = ApiPath(s"${base.repo.url.path}/issues/${number}/comments")
val statuses_url = ApiPath(s"${base.repo.url.path}/statuses/${head.sha}")
}
object ApiPullRequest{
def apply(issue: Issue, pullRequest: PullRequest, headRepo: ApiRepository, baseRepo: ApiRepository, user: ApiUser): ApiPullRequest = ApiPullRequest(
number = issue.issueId,
updated_at = issue.updatedDate,
created_at = issue.registeredDate,
head = Commit(
sha = pullRequest.commitIdTo,
ref = pullRequest.requestBranch,
repo = headRepo)(issue.userName),
base = Commit(
sha = pullRequest.commitIdFrom,
ref = pullRequest.branch,
repo = baseRepo)(issue.userName),
mergeable = None, // TODO: need check mergeable.
title = issue.title,
body = issue.content.getOrElse(""),
user = user
)
case class Commit(
sha: String,
ref: String,
repo: ApiRepository)(baseOwner:String){
val label = if( baseOwner == repo.owner.login ){ ref }else{ s"${repo.owner.login}:${ref}" }
val user = repo.owner
}
}

View File

@@ -0,0 +1,48 @@
package gitbucket.core.api
import gitbucket.core.model.{Account, Repository}
import gitbucket.core.service.RepositoryService.RepositoryInfo
// https://developer.github.com/v3/repos/
case class ApiRepository(
name: String,
full_name: String,
description: String,
watchers: Int,
forks: Int,
`private`: Boolean,
default_branch: String,
owner: ApiUser) {
val forks_count = forks
val watchers_coun = watchers
val url = ApiPath(s"/api/v3/repos/${full_name}")
val http_url = ApiPath(s"/git/${full_name}.git")
val clone_url = ApiPath(s"/git/${full_name}.git")
val html_url = ApiPath(s"/${full_name}")
}
object ApiRepository{
def apply(
repository: Repository,
owner: ApiUser,
forkedCount: Int =0,
watchers: Int = 0): ApiRepository =
ApiRepository(
name = repository.repositoryName,
full_name = s"${repository.userName}/${repository.repositoryName}",
description = repository.description.getOrElse(""),
watchers = 0,
forks = forkedCount,
`private` = repository.isPrivate,
default_branch = repository.defaultBranch,
owner = owner
)
def apply(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount)
def apply(repositoryInfo: RepositoryInfo, owner: Account): ApiRepository =
this(repositoryInfo.repository, ApiUser(owner))
}

View File

@@ -0,0 +1,36 @@
package gitbucket.core.api
import gitbucket.core.model.Account
import java.util.Date
case class ApiUser(
login: String,
email: String,
`type`: String,
site_admin: Boolean,
created_at: Date) {
val url = ApiPath(s"/api/v3/users/${login}")
val html_url = ApiPath(s"/${login}")
// val followers_url = ApiPath(s"/api/v3/users/${login}/followers")
// val following_url = ApiPath(s"/api/v3/users/${login}/following{/other_user}")
// val gists_url = ApiPath(s"/api/v3/users/${login}/gists{/gist_id}")
// val starred_url = ApiPath(s"/api/v3/users/${login}/starred{/owner}{/repo}")
// val subscriptions_url = ApiPath(s"/api/v3/users/${login}/subscriptions")
// val organizations_url = ApiPath(s"/api/v3/users/${login}/orgs")
// val repos_url = ApiPath(s"/api/v3/users/${login}/repos")
// val events_url = ApiPath(s"/api/v3/users/${login}/events{/privacy}")
// val received_events_url = ApiPath(s"/api/v3/users/${login}/received_events")
}
object ApiUser{
def apply(user: Account): ApiUser = ApiUser(
login = user.fullName,
email = user.mailAddress,
`type` = if(user.isGroupAccount){ "Organization" }else{ "User" },
site_admin = user.isAdmin,
created_at = user.registeredDate
)
}

View File

@@ -0,0 +1,7 @@
package gitbucket.core.api
/**
* https://developer.github.com/v3/issues/comments/#create-a-comment
* api form
*/
case class CreateAComment(body: String)

View File

@@ -0,0 +1,26 @@
package gitbucket.core.api
import gitbucket.core.model.CommitState
/**
* https://developer.github.com/v3/repos/statuses/#create-a-status
* api form
*/
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 &&
// only http
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
}
}

View File

@@ -0,0 +1,44 @@
package gitbucket.core.api
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import org.joda.time.format._
import org.json4s._
import org.json4s.jackson.Serialization
import java.util.Date
import scala.util.Try
object JsonFormat {
case class Context(baseUrl:String)
val parserISO = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
val jsonFormats = Serialization.formats(NoTypeHints) + new CustomSerializer[Date](format =>
(
{ case JString(s) => Try(parserISO.parseDateTime(s)).toOption.map(_.toDate)
.getOrElse(throw new MappingException("Can't convert " + s + " to Date")) },
{ case x: Date => JString(parserISO.print(new DateTime(x).withZone(DateTimeZone.UTC))) }
)
) + FieldSerializer[ApiUser]() + FieldSerializer[ApiPullRequest]() + FieldSerializer[ApiRepository]() +
FieldSerializer[ApiCommitListItem.Parent]() + FieldSerializer[ApiCommitListItem]() + FieldSerializer[ApiCommitListItem.Commit]() +
FieldSerializer[ApiCommitStatus]() + FieldSerializer[ApiCommit]() + FieldSerializer[ApiCombinedCommitStatus]() +
FieldSerializer[ApiPullRequest.Commit]()
def apiPathSerializer(c: Context) = 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 ApiPath")
},
{
case ApiPath(path) => JString(c.baseUrl+path)
}
)
)
/**
* convert object to json string
*/
def apply(obj: AnyRef)(implicit c: Context): String = Serialization.write(obj)(jsonFormats + apiPathSerializer(c))
}

View File

@@ -1,30 +1,35 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.account.html import gitbucket.core.account.html
import gitbucket.core.api._
import gitbucket.core.helper import gitbucket.core.helper
import gitbucket.core.model.GroupMember import gitbucket.core.model.GroupMember
import gitbucket.core.util._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.StringUtil._
import gitbucket.core.ssh.SshUtil
import gitbucket.core.service._ import gitbucket.core.service._
import gitbucket.core.ssh.SshUtil
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.StringUtil._
import gitbucket.core.util._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.{FileMode, Constants}
import org.eclipse.jgit.dircache.DirCache import org.eclipse.jgit.dircache.DirCache
import org.eclipse.jgit.lib.{FileMode, Constants}
import org.scalatra.i18n.Messages
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 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 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])
@@ -34,6 +39,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
case class SshKeyForm(title: String, publicKey: String) case class SshKeyForm(title: String, publicKey: String)
case class PersonalTokenForm(note: String)
val newForm = mapping( val newForm = mapping(
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))), "userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" , text(required, maxlength(20)))), "password" -> trim(label("Password" , text(required, maxlength(20)))),
@@ -57,6 +64,10 @@ trait AccountControllerBase extends AccountManagementControllerBase {
"publicKey" -> trim(label("Key" , text(required, validPublicKey))) "publicKey" -> trim(label("Key" , text(required, validPublicKey)))
)(SshKeyForm.apply) )(SshKeyForm.apply)
val personalTokenForm = mapping(
"note" -> trim(label("Token", text(required, maxlength(100))))
)(PersonalTokenForm.apply)
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String) case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String)
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String, clearImage: Boolean) case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String, clearImage: Boolean)
@@ -145,6 +156,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 =>
JsonFormat(ApiUser(account))
} getOrElse NotFound
}
/**
* https://developer.github.com/v3/users/#get-the-authenticated-user
*/
get("/api/v3/user") {
context.loginAccount.map { account =>
JsonFormat(ApiUser(account))
} getOrElse Unauthorized
}
get("/:userName/_edit")(oneselfOnly { get("/:userName/_edit")(oneselfOnly {
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName).map { x => getAccountByUserName(userName).map { x =>
@@ -209,6 +239,40 @@ trait AccountControllerBase extends AccountManagementControllerBase {
redirect(s"/${userName}/_ssh") redirect(s"/${userName}/_ssh")
}) })
get("/:userName/_application")(oneselfOnly {
val userName = params("userName")
getAccountByUserName(userName).map { x =>
var tokens = getAccessTokens(x.userName)
val generatedToken = flash.get("generatedToken") match {
case Some((tokenId:Int, token:String)) => {
val gt = tokens.find(_.accessTokenId == tokenId)
gt.map{ t =>
tokens = tokens.filterNot(_ == t)
(t, token)
}
}
case _ => None
}
html.application(x, tokens, generatedToken)
} getOrElse NotFound
})
post("/:userName/_personalToken", personalTokenForm)(oneselfOnly { form =>
val userName = params("userName")
getAccountByUserName(userName).map { x =>
val (tokenId, token) = generateAccessToken(userName, form.note)
flash += "generatedToken" -> (tokenId, token)
}
redirect(s"/${userName}/_application")
})
get("/:userName/_personalToken/delete/:id")(oneselfOnly {
val userName = params("userName")
val tokenId = params("id").toInt
deleteAccessToken(userName, tokenId)
redirect(s"/${userName}/_application")
})
get("/register"){ get("/register"){
if(context.settings.allowAccountRegistration){ if(context.settings.allowAccountRegistration){
if(context.loginAccount.isDefined){ if(context.loginAccount.isDefined){

View File

@@ -1,19 +1,25 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.api.ApiError
import gitbucket.core.model.Account
import gitbucket.core.service.{AccountService, SystemSettingsService} import gitbucket.core.service.{AccountService, SystemSettingsService}
import gitbucket.core.util._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
import gitbucket.core.model.Account import gitbucket.core.util.Implicits._
import org.scalatra._ import gitbucket.core.util._
import org.scalatra.json._
import org.json4s._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.json4s._
import org.scalatra._
import org.scalatra.i18n._
import org.scalatra.json._
import javax.servlet.http.{HttpServletResponse, HttpServletRequest} import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import javax.servlet.{FilterChain, ServletResponse, ServletRequest} import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
import org.scalatra.i18n._
import scala.util.Try
/** /**
* Provides generic features for controller implementations. * Provides generic features for controller implementations.
@@ -51,6 +57,9 @@ abstract class ControllerBase extends ScalatraFilter
// Git repository // Git repository
chain.doFilter(request, response) chain.doFilter(request, response)
} else { } else {
if(path.startsWith("/api/v3/")){
httpRequest.setAttribute(Keys.Request.APIv3, true)
}
// Scalatra actions // Scalatra actions
super.doFilter(request, response, chain) super.doFilter(request, response, chain)
} }
@@ -74,7 +83,7 @@ abstract class ControllerBase extends ScalatraFilter
} }
} }
private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount) private def LoginAccount: Option[Account] = request.getAs[Account](Keys.Session.LoginAccount).orElse(session.getAs[Account](Keys.Session.LoginAccount))
def ajaxGet(path : String)(action : => Any) : Route = def ajaxGet(path : String)(action : => Any) : Route =
super.get(path){ super.get(path){
@@ -103,6 +112,9 @@ abstract class ControllerBase extends ScalatraFilter
protected def NotFound() = protected def NotFound() =
if(request.hasAttribute(Keys.Request.Ajax)){ if(request.hasAttribute(Keys.Request.Ajax)){
org.scalatra.NotFound() org.scalatra.NotFound()
} else if(request.hasAttribute(Keys.Request.APIv3)){
contentType = formats("json")
org.scalatra.NotFound(ApiError("Not Found"))
} else { } else {
org.scalatra.NotFound(gitbucket.core.html.error("Not Found")) org.scalatra.NotFound(gitbucket.core.html.error("Not Found"))
} }
@@ -110,6 +122,9 @@ abstract class ControllerBase extends ScalatraFilter
protected def Unauthorized()(implicit context: Context) = protected def Unauthorized()(implicit context: Context) =
if(request.hasAttribute(Keys.Request.Ajax)){ if(request.hasAttribute(Keys.Request.Ajax)){
org.scalatra.Unauthorized() org.scalatra.Unauthorized()
} else if(request.hasAttribute(Keys.Request.APIv3)){
contentType = formats("json")
org.scalatra.Unauthorized(ApiError("Requires authentication"))
} else { } else {
if(context.loginAccount.isDefined){ if(context.loginAccount.isDefined){
org.scalatra.Unauthorized(redirect("/")) org.scalatra.Unauthorized(redirect("/"))
@@ -146,6 +161,15 @@ abstract class ControllerBase extends ScalatraFilter
response.addHeader("X-Content-Type-Options", "nosniff") response.addHeader("X-Content-Type-Options", "nosniff")
rawData rawData
} }
// jenkins send message as 'application/x-www-form-urlencoded' but scalatra already parsed as multi-part-request.
def extractFromJsonBody[A](implicit request:HttpServletRequest, mf:Manifest[A]): Option[A] = {
(request.contentType.map(_.split(";").head.toLowerCase) match{
case Some("application/x-www-form-urlencoded") => multiParams.keys.headOption.map(parse(_))
case Some("application/json") => Some(parsedBody)
case _ => Some(parse(request.body))
}).filterNot(_ == JNothing).flatMap(j => Try(j.extract[A]).toOption)
}
} }
/** /**

View File

@@ -1,16 +1,20 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.html import gitbucket.core.api._
import gitbucket.core.helper.xml import gitbucket.core.helper.xml
import gitbucket.core.html
import gitbucket.core.model.Account import gitbucket.core.model.Account
import gitbucket.core.service.{RepositoryService, ActivityService, AccountService} import gitbucket.core.service.{RepositoryService, ActivityService, AccountService}
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator}
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator}
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
class IndexController extends IndexControllerBase class IndexController extends IndexControllerBase
with RepositoryService with ActivityService with AccountService with UsersAuthenticator with RepositoryService with ActivityService with AccountService with UsersAuthenticator
trait IndexControllerBase extends ControllerBase { trait IndexControllerBase extends ControllerBase {
self: RepositoryService with ActivityService with AccountService with UsersAuthenticator => self: RepositoryService with ActivityService with AccountService with UsersAuthenticator =>
@@ -106,4 +110,13 @@ trait IndexControllerBase extends ControllerBase {
getAccountByUserName(params("userName")).isDefined getAccountByUserName(params("userName")).isDefined
}) })
/**
* @see https://developer.github.com/v3/rate_limit/#get-your-current-rate-limit-status
* but not enabled.
*/
get("/api/v3/rate_limit"){
contentType = formats("json")
// this message is same as github enterprise...
org.scalatra.NotFound(ApiError("Rate limiting is not enabled."))
}
} }

View File

@@ -1,25 +1,27 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.api._
import gitbucket.core.issues.html import gitbucket.core.issues.html
import gitbucket.core.model.Issue import gitbucket.core.model.Issue
import gitbucket.core.service.IssuesService._
import gitbucket.core.service._ import gitbucket.core.service._
import gitbucket.core.util._
import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util._
import gitbucket.core.view import gitbucket.core.view
import gitbucket.core.view.Markdown import gitbucket.core.view.Markdown
import jp.sf.amateras.scalatra.forms._
import IssuesService._ import jp.sf.amateras.scalatra.forms._
import org.scalatra.Ok import org.scalatra.Ok
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
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService
trait IssuesControllerBase extends ControllerBase { trait IssuesControllerBase extends ControllerBase {
self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator => with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService =>
case class IssueCreateForm(title: String, content: Option[String], case class IssueCreateForm(title: String, content: Option[String],
assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String]) assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
@@ -76,6 +78,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 {
JsonFormat(comments.map{ case (issueComment, user) => ApiComment(issueComment, ApiUser(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) =>
html.create( html.create(
@@ -112,9 +126,12 @@ trait IssuesControllerBase extends ControllerBase {
// record activity // record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title) recordCreateIssueActivity(owner, name, userName, issueId, form.title)
// extract references and create refer comment
getIssue(owner, name, issueId.toString).foreach { issue => getIssue(owner, name, issueId.toString).foreach { issue =>
// extract references and create refer comment
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
// call web hooks
callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get)
} }
// notifications // notifications
@@ -163,6 +180,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 =>
(for{
issueId <- params("id").toIntOpt
body <- extractFromJsonBody[CreateAComment].map(_.body) if ! body.isEmpty
(issue, id) <- handleComment(issueId, Some(body), repository)()
issueComment <- getComment(repository.owner, repository.name, id.toString())
} yield {
JsonFormat(ApiComment(issueComment, ApiUser(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}/${
@@ -367,6 +398,22 @@ trait IssuesControllerBase extends ControllerBase {
createReferComment(owner, name, issue, content) createReferComment(owner, name, issue, content)
} }
// call web hooks
action match {
case None => commentId.map{ commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, context.loginAccount.get) }
case Some(act) => val webHookAction = act match {
case "open" => "opened"
case "reopen" => "reopened"
case "close" => "closed"
case _ => act
}
if(issue.isPullRequest){
callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, context.loginAccount.get)
} else {
callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, context.loginAccount.get)
}
}
// notifications // notifications
Notifier() match { Notifier() match {
case f => case f =>
@@ -419,5 +466,4 @@ trait IssuesControllerBase extends ControllerBase {
hasWritePermission(owner, repoName, context.loginAccount)) hasWritePermission(owner, repoName, context.loginAccount))
} }
} }
} }

View File

@@ -1,34 +1,39 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.api._
import gitbucket.core.model.{Account, CommitState, Repository, PullRequest, Issue}
import gitbucket.core.pulls.html import gitbucket.core.pulls.html
import gitbucket.core.util._ import gitbucket.core.service.CommitStatusService
import gitbucket.core.util.JGitUtil._ import gitbucket.core.service.MergeService
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import gitbucket.core.view
import gitbucket.core.view.helpers
import org.eclipse.jgit.api.Git
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.transport.RefSpec
import scala.collection.JavaConverters._
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
import gitbucket.core.service._
import gitbucket.core.service.IssuesService._ import gitbucket.core.service.IssuesService._
import gitbucket.core.service.PullRequestService._ import gitbucket.core.service.PullRequestService._
import gitbucket.core.service.WebHookService.WebHookPayload import gitbucket.core.service._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.JGitUtil._
import gitbucket.core.util._
import gitbucket.core.view
import gitbucket.core.view.helpers
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.PersonIdent
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.eclipse.jgit.merge.MergeStrategy
import org.eclipse.jgit.errors.NoMergeBaseException 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 WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator with CommitsService with ActivityService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
with CommitStatusService with MergeService
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 WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator => with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
with CommitStatusService with MergeService =>
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase]) private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
@@ -70,6 +75,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[(Issue, Account, Int, PullRequest, Repository, Account)] = searchPullRequestByApi(condition, (page - 1) * PullRequestLimit, PullRequestLimit, repository.owner -> repository.name)
JsonFormat(issues.map{case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner) =>
ApiPullRequest(
issue,
pullRequest,
ApiRepository(headRepo, ApiUser(headOwner)),
ApiRepository(repository, ApiUser(baseOwner)),
ApiUser(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
@@ -78,7 +101,6 @@ trait PullRequestsControllerBase extends ControllerBase {
using(Git.open(getRepositoryDir(owner, name))){ git => using(Git.open(getRepositoryDir(owner, name))){ git =>
val (commits, diffs) = val (commits, diffs) =
getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo) getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
html.pullreq( html.pullreq(
issue, pullreq, issue, pullreq,
(commits.flatten.map(commit => getCommitComments(owner, name, commit.id, true)).flatten.toList ::: getComments(owner, name, issueId)) (commits.flatten.map(commit => getCommitComments(owner, name, commit.id, true)).flatten.toList ::: getComments(owner, name, issueId))
@@ -96,14 +118,64 @@ 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 {
JsonFormat(ApiPullRequest(
issue,
pullRequest,
ApiRepository(headRepo, ApiUser(headOwner)),
ApiRepository(repository, ApiUser(baseOwner)),
ApiUser(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 = RepositoryName(repository)
val commits = git.log.addRange(oldId, newId).call.iterator.asScala.map(c => ApiCommitListItem(new CommitInfo(c), repoFullName)).toList
JsonFormat(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
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 hasConfrict = LockUtil.lock(s"${owner}/${name}"){
checkConflict(owner, name, pullreq.branch, issueId)
}
val hasProblem = hasConfrict || (!statuses.isEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS)
html.mergeguide( html.mergeguide(
checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId), hasConfrict,
hasProblem,
issue,
pullreq, pullreq,
statuses,
repository,
s"${context.baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git") s"${context.baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
} }
} getOrElse NotFound } getOrElse NotFound
@@ -140,43 +212,10 @@ trait PullRequestsControllerBase extends ControllerBase {
// record activity // record activity
recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message) recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
// merge // merge git repository
val mergeBaseRefName = s"refs/heads/${pullreq.branch}" mergePullRequest(git, pullreq.branch, issueId,
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true) s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestBranch}\n\n" + form.message,
val mergeBaseTip = git.getRepository.resolve(mergeBaseRefName) new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
val conflicted = try {
!merger.merge(mergeBaseTip, mergeTip)
} catch {
case e: NoMergeBaseException => true
}
if (conflicted) {
throw new RuntimeException("This pull request can't merge automatically.")
}
// creates merge commit
val mergeCommit = new CommitBuilder()
mergeCommit.setTreeId(merger.getResultTreeId)
mergeCommit.setParentIds(Array[ObjectId](mergeBaseTip, mergeTip): _*)
val personIdent = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
mergeCommit.setAuthor(personIdent)
mergeCommit.setCommitter(personIdent)
mergeCommit.setMessage(s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestBranch}\n\n" +
form.message)
// insertObject and got mergeCommit Object Id
val inserter = git.getRepository.newObjectInserter
val mergeCommitId = inserter.insert(mergeCommit)
inserter.flush()
inserter.release()
// update refs
val refUpdate = git.getRepository.updateRef(mergeBaseRefName)
refUpdate.setNewObjectId(mergeCommitId)
refUpdate.setForceUpdate(false)
refUpdate.setRefLogIdent(personIdent)
refUpdate.setRefLogMessage("merged", true)
refUpdate.update()
val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom, val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo) pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
@@ -194,14 +233,7 @@ trait PullRequestsControllerBase extends ControllerBase {
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name) closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
} }
// call web hook // call web hook
getWebHookURLs(owner, name) match { callPullRequestWebHook("closed", repository, issueId, context.baseUrl, context.loginAccount.get)
case webHookURLs if(webHookURLs.nonEmpty) =>
for(ownerAccount <- getAccountByUserName(owner)){
callWebHook(owner, name, webHookURLs,
WebHookPayload(git, loginAccount, mergeBaseRefName, repository, commits.flatten.toList, ownerAccount))
}
case _ =>
}
// notifications // notifications
Notifier().toNotify(repository, issueId, "merge"){ Notifier().toNotify(repository, issueId, "merge"){
@@ -319,10 +351,11 @@ trait PullRequestsControllerBase extends ControllerBase {
){ case (oldGit, newGit) => ){ case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2 val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2 val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
val conflict = LockUtil.lock(s"${originRepository.owner}/${originRepository.name}"){
html.mergecheck(
checkConflict(originRepository.owner, originRepository.name, originBranch, checkConflict(originRepository.owner, originRepository.name, originBranch,
forkedRepository.owner, forkedRepository.name, forkedBranch)) forkedRepository.owner, forkedRepository.name, forkedBranch)
}
html.mergecheck(conflict)
} }
}) getOrElse NotFound }) getOrElse NotFound
}) })
@@ -352,16 +385,14 @@ trait PullRequestsControllerBase extends ControllerBase {
commitIdTo = form.commitIdTo) commitIdTo = form.commitIdTo)
// fetch requested branch // fetch requested branch
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => fetchAsPullRequest(repository.owner, repository.name, form.requestUserName, form.requestRepositoryName, form.requestBranch, issueId)
git.fetch
.setRemote(getRepositoryDir(form.requestUserName, form.requestRepositoryName).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head"))
.call
}
// record activity // record activity
recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title) recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title)
// call web hook
callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get)
// notifications // notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){ Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}") Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
@@ -370,62 +401,6 @@ trait PullRequestsControllerBase extends ControllerBase {
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
}) })
/**
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
*/
private def checkConflict(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
LockUtil.lock(s"${userName}/${repositoryName}"){
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
val remoteRefName = s"refs/heads/${branch}"
val tmpRefName = s"refs/merge-check/${userName}/${branch}"
val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true)
try {
// fetch objects from origin repository branch
git.fetch
.setRemote(getRepositoryDir(userName, repositoryName).toURI.toString)
.setRefSpecs(refSpec)
.call
// merge conflict check
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${requestBranch}")
val mergeTip = git.getRepository.resolve(tmpRefName)
try {
!merger.merge(mergeBaseTip, mergeTip)
} catch {
case e: NoMergeBaseException => true
}
} finally {
val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
refUpdate.setForceUpdate(true)
refUpdate.delete()
}
}
}
}
/**
* Checks whether conflict will be caused in merging within pull request. Returns true if conflict will be caused.
*/
private def checkConflictInPullRequest(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String,
issueId: Int): Boolean = {
LockUtil.lock(s"${userName}/${repositoryName}") {
using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
// merge
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${branch}")
val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
try {
!merger.merge(mergeBaseTip, mergeTip)
} catch {
case e: NoMergeBaseException => true
}
}
}
}
/** /**
* Parses branch identifier and extracts owner and branch name as tuple. * Parses branch identifier and extracts owner and branch name as tuple.
* *
@@ -484,5 +459,4 @@ trait PullRequestsControllerBase extends ControllerBase {
repository, repository,
hasWritePermission(owner, repoName, context.loginAccount)) hasWritePermission(owner, repoName, context.loginAccount))
} }
} }

View File

@@ -3,7 +3,7 @@ package gitbucket.core.controller
import gitbucket.core.settings.html import gitbucket.core.settings.html
import gitbucket.core.model.WebHook import gitbucket.core.model.WebHook
import gitbucket.core.service.{RepositoryService, AccountService, WebHookService} import gitbucket.core.service.{RepositoryService, AccountService, WebHookService}
import gitbucket.core.service.WebHookService.WebHookPayload import gitbucket.core.service.WebHookService._
import gitbucket.core.util._ import gitbucket.core.util._
import gitbucket.core.util.JGitUtil._ import gitbucket.core.util.JGitUtil._
import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.ControlUtil._
@@ -15,6 +15,7 @@ import org.scalatra.i18n.Messages
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Constants import org.eclipse.jgit.lib.Constants
class RepositorySettingsController extends RepositorySettingsControllerBase class RepositorySettingsController extends RepositorySettingsControllerBase
with RepositoryService with AccountService with WebHookService with RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator with OwnerAuthenticator with UsersAuthenticator
@@ -168,9 +169,9 @@ trait RepositorySettingsControllerBase extends ControllerBase {
.call.iterator.asScala.map(new CommitInfo(_)) .call.iterator.asScala.map(new CommitInfo(_))
getAccountByUserName(repository.owner).foreach { ownerAccount => getAccountByUserName(repository.owner).foreach { ownerAccount =>
callWebHook(repository.owner, repository.name, callWebHook("push",
List(WebHook(repository.owner, repository.name, form.url)), List(WebHook(repository.owner, repository.name, form.url)),
WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount) WebHookPushPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)
) )
} }
flash += "url" -> form.url flash += "url" -> form.url

View File

@@ -1,5 +1,6 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.api._
import gitbucket.core.repo.html import gitbucket.core.repo.html
import gitbucket.core.helper import gitbucket.core.helper
import gitbucket.core.service._ import gitbucket.core.service._
@@ -8,24 +9,27 @@ import gitbucket.core.util.JGitUtil._
import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
import gitbucket.core.model.Account import gitbucket.core.model.{Account, CommitState}
import gitbucket.core.service.WebHookService.WebHookPayload import gitbucket.core.service.CommitStatusService
import gitbucket.core.service.WebHookService._
import gitbucket.core.view import gitbucket.core.view
import gitbucket.core.view.helpers import gitbucket.core.view.helpers
import org.scalatra._
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.{ArchiveCommand, Git} import org.eclipse.jgit.api.{ArchiveCommand, Git}
import org.eclipse.jgit.archive.{TgzFormat, ZipFormat} import org.eclipse.jgit.archive.{TgzFormat, ZipFormat}
import org.eclipse.jgit.lib._
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.treewalk._
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.dircache.DirCache import org.eclipse.jgit.dircache.DirCache
import org.eclipse.jgit.lib._
import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevCommit
import org.eclipse.jgit.treewalk._
import org.scalatra._
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
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
with WebHookPullRequestService
/** /**
@@ -33,7 +37,8 @@ class RepositoryViewerController extends RepositoryViewerControllerBase
*/ */
trait RepositoryViewerControllerBase extends ControllerBase { trait RepositoryViewerControllerBase extends ControllerBase {
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService 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
with WebHookPullRequestService =>
ArchiveCommand.registerFormat("zip", new ZipFormat) ArchiveCommand.registerFormat("zip", new ZipFormat)
ArchiveCommand.registerFormat("tar.gz", new TgzFormat) ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
@@ -109,6 +114,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
fileList(_) fileList(_)
}) })
/**
* https://developer.github.com/v3/repos/#get
*/
get("/api/v3/repos/:owner/:repository")(referrersOnly { repository =>
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(repository.owner).get)))
})
/** /**
* Displays the file list of the specified path and branch. * Displays the file list of the specified path and branch.
*/ */
@@ -140,6 +152,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 <- 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 {
JsonFormat(ApiCommitStatus(status, ApiUser(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 {
JsonFormat(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map{ case(status, creator) =>
ApiCommitStatus(status, ApiUser(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)
JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner)))
}) getOrElse NotFound
})
get("/:owner/:repository/new/*")(collaboratorsOnly { repository => get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head) val (branch, path) = splitPath(repository, multiParams("splat").head)
html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList, html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
@@ -507,14 +569,12 @@ trait RepositoryViewerControllerBase extends ControllerBase {
closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name) closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name)
// call web hook // call web hook
callPullRequestWebHookByRequestBranch("synchronize", repository, branch, context.baseUrl, loginAccount)
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId)) val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
getWebHookURLs(repository.owner, repository.name) match { callWebHookOf(repository.owner, repository.name, "push") {
case webHookURLs if(webHookURLs.nonEmpty) => getAccountByUserName(repository.owner).map{ ownerAccount =>
for(ownerAccount <- getAccountByUserName(repository.owner)){ WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount)
callWebHook(repository.owner, repository.name, webHookURLs,
WebHookPayload(git, loginAccount, headName, repository, List(commit), ownerAccount))
} }
case _ =>
} }
} }
} }

View File

@@ -0,0 +1,21 @@
package gitbucket.core.model
trait AccessTokenComponent { self: Profile =>
import profile.simple._
lazy val AccessTokens = TableQuery[AccessTokens]
class AccessTokens(tag: Tag) extends Table[AccessToken](tag, "ACCESS_TOKEN") {
val accessTokenId = column[Int]("ACCESS_TOKEN_ID", O AutoInc)
val userName = column[String]("USER_NAME")
val tokenHash = column[String]("TOKEN_HASH")
val note = column[String]("NOTE")
def * = (accessTokenId, userName, tokenHash, note) <> (AccessToken.tupled, AccessToken.unapply)
}
}
case class AccessToken(
accessTokenId: Int = 0,
userName: String,
tokenHash: String,
note: String
)

View File

@@ -49,6 +49,9 @@ protected[model] trait TemplateComponent { self: Profile =>
def byCommit(owner: String, repository: String, commitId: String) = def byCommit(owner: String, repository: String, commitId: String) =
byRepository(owner, repository) && (this.commitId === commitId) byRepository(owner, repository) && (this.commitId === commitId)
def byCommit(owner: Column[String], repository: Column[String], commitId: Column[String]) =
byRepository(userName, repositoryName) && (this.commitId === commitId)
} }
} }

View File

@@ -0,0 +1,83 @@
package gitbucket.core.model
import scala.slick.lifted.MappedTo
import scala.slick.jdbc._
trait CommitStatusComponent extends TemplateComponent { self: Profile =>
import profile.simple._
import self._
implicit val commitStateColumnType = MappedColumnType.base[CommitState, String](b => b.name , i => CommitState(i))
lazy val CommitStatuses = TableQuery[CommitStatuses]
class CommitStatuses(tag: Tag) extends Table[CommitStatus](tag, "COMMIT_STATUS") with CommitTemplate {
val commitStatusId = column[Int]("COMMIT_STATUS_ID", O AutoInc)
val context = column[String]("CONTEXT")
val state = column[CommitState]("STATE")
val targetUrl = column[Option[String]]("TARGET_URL")
val description = column[Option[String]]("DESCRIPTION")
val creator = column[String]("CREATOR")
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
val updatedDate = column[java.util.Date]("UPDATED_DATE")
def * = (commitStatusId, userName, repositoryName, commitId, context, state, targetUrl, description, creator, registeredDate, updatedDate) <> (CommitStatus.tupled, CommitStatus.unapply)
def byPrimaryKey(id: Int) = commitStatusId === id.bind
}
}
case class CommitStatus(
commitStatusId: Int = 0,
userName: String,
repositoryName: String,
commitId: String,
context: String,
state: CommitState,
targetUrl: Option[String],
description: Option[String],
creator: String,
registeredDate: java.util.Date,
updatedDate: java.util.Date
)
sealed abstract class CommitState(val name: String)
object CommitState {
object ERROR extends CommitState("error")
object FAILURE extends CommitState("failure")
object PENDING extends CommitState("pending")
object SUCCESS extends CommitState("success")
val values: Vector[CommitState] = Vector(PENDING, SUCCESS, ERROR, FAILURE)
private val map: Map[String, CommitState] = values.map(enum => enum.name -> enum).toMap
def apply(name: String): CommitState = map(name)
def valueOf(name: String): Option[CommitState] = map.get(name)
/**
* failure if any of the contexts report as error or failure
* pending if there are no statuses or a context is pending
* success if the latest status for all contexts is success
*/
def combine(statuses: Set[CommitState]): CommitState = {
if(statuses.isEmpty){
PENDING
} else if(statuses.contains(CommitState.ERROR) || statuses.contains(CommitState.FAILURE)) {
FAILURE
} else if(statuses.contains(CommitState.PENDING)) {
PENDING
} else {
SUCCESS
}
}
implicit val getResult: GetResult[CommitState] = GetResult(r => CommitState(r.<<))
implicit val getResultOpt: GetResult[Option[CommitState]] = GetResult(r => r.<<?[String].map(CommitState(_)))
}

View File

@@ -1,5 +1,6 @@
package gitbucket.core.model package gitbucket.core.model
trait Profile { trait Profile {
val profile: slick.driver.JdbcProfile val profile: slick.driver.JdbcProfile
import profile.simple._ import profile.simple._
@@ -31,10 +32,12 @@ trait ProfileProvider { self: Profile =>
} }
trait CoreProfile extends ProfileProvider with Profile trait CoreProfile extends ProfileProvider with Profile
with AccessTokenComponent
with AccountComponent with AccountComponent
with ActivityComponent with ActivityComponent
with CollaboratorComponent with CollaboratorComponent
with CommitCommentComponent with CommitCommentComponent
with CommitStatusComponent
with GroupMemberComponent with GroupMemberComponent
with IssueComponent with IssueComponent
with IssueCommentComponent with IssueCommentComponent

View File

@@ -0,0 +1,55 @@
package gitbucket.core.service
import gitbucket.core.model.Profile._
import profile.simple._
import gitbucket.core.model.{Account, AccessToken}
import gitbucket.core.util.StringUtil
import scala.util.Random
trait AccessTokenService {
def makeAccessTokenString: String = {
val bytes = new Array[Byte](20)
Random.nextBytes(bytes)
bytes.map("%02x".format(_)).mkString
}
def tokenToHash(token: String): String = StringUtil.sha1(token)
/**
* @retuen (TokenId, Token)
*/
def generateAccessToken(userName: String, note: String)(implicit s: Session): (Int, String) = {
var token: String = null
var hash: String = null
do{
token = makeAccessTokenString
hash = tokenToHash(token)
}while(AccessTokens.filter(_.tokenHash === hash.bind).exists.run)
val newToken = AccessToken(
userName = userName,
note = note,
tokenHash = hash)
val tokenId = (AccessTokens returning AccessTokens.map(_.accessTokenId)) += newToken
(tokenId, token)
}
def getAccountByAccessToken(token: String)(implicit s: Session): Option[Account] =
Accounts
.innerJoin(AccessTokens)
.filter{ case (ac, t) => (ac.userName === t.userName) && (t.tokenHash === tokenToHash(token).bind) && (ac.removed === false.bind) }
.map{ case (ac, t) => ac }
.firstOption
def getAccessTokens(userName: String)(implicit s: Session): List[AccessToken] =
AccessTokens.filter(_.userName === userName.bind).sortBy(_.accessTokenId.desc).list
def deleteAccessToken(userName: String, accessTokenId: Int)(implicit s: Session): Unit =
AccessTokens filter (t => t.userName === userName.bind && t.accessTokenId === accessTokenId) delete
}
object AccessTokenService extends AccessTokenService

View File

@@ -77,6 +77,16 @@ trait AccountService {
def getAccountByUserName(userName: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] = def getAccountByUserName(userName: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] =
Accounts filter(t => (t.userName === userName.bind) && (t.removed === false.bind, !includeRemoved)) firstOption Accounts filter(t => (t.userName === userName.bind) && (t.removed === false.bind, !includeRemoved)) firstOption
def getAccountsByUserNames(userNames: Set[String], knowns:Set[Account], includeRemoved: Boolean = false)(implicit s: Session): Map[String, Account] = {
val map = knowns.map(a => a.userName -> a).toMap
val needs = userNames -- map.keySet
if(needs.isEmpty){
map
}else{
map ++ Accounts.filter(t => (t.userName inSetBind needs) && (t.removed === false.bind, !includeRemoved)).list.map(a => a.userName -> a).toMap
}
}
def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] = def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] =
Accounts filter(t => (t.mailAddress.toLowerCase === mailAddress.toLowerCase.bind) && (t.removed === false.bind, !includeRemoved)) firstOption Accounts filter(t => (t.mailAddress.toLowerCase === mailAddress.toLowerCase.bind) && (t.removed === false.bind, !includeRemoved)) firstOption

View File

@@ -0,0 +1,52 @@
package gitbucket.core.service
import gitbucket.core.model.Profile._
import profile.simple._
import gitbucket.core.model.{CommitState, CommitStatus, Account}
import gitbucket.core.util.Implicits._
import gitbucket.core.util.StringUtil._
import gitbucket.core.service.RepositoryService.RepositoryInfo
trait CommitStatusService {
/** insert or update */
def createCommitStatus(userName: String, repositoryName: String, sha:String, context:String, state:CommitState, targetUrl:Option[String], description:Option[String], now:java.util.Date, creator:Account)(implicit s: Session): Int =
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha) && t.context===context.bind )
.map(_.commitStatusId).firstOption match {
case Some(id:Int) => {
CommitStatuses.filter(_.byPrimaryKey(id)).map{
t => (t.state , t.targetUrl , t.updatedDate , t.creator, t.description)
}.update( (state, targetUrl, now, creator.userName, description) )
id
}
case None => (CommitStatuses returning CommitStatuses.map(_.commitStatusId)) += CommitStatus(
userName = userName,
repositoryName = repositoryName,
commitId = sha,
context = context,
state = state,
targetUrl = targetUrl,
description = description,
creator = creator.userName,
registeredDate = now,
updatedDate = now)
}
def getCommitStatus(userName: String, repositoryName: String, id: Int)(implicit s: Session) :Option[CommitStatus] =
CommitStatuses.filter(t => t.byPrimaryKey(id) && t.byRepository(userName, repositoryName)).firstOption
def getCommitStatus(userName: String, repositoryName: String, sha: String, context: String)(implicit s: Session) :Option[CommitStatus] =
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha) && t.context===context.bind ).firstOption
def getCommitStatues(userName: String, repositoryName: String, sha: String)(implicit s: Session) :List[CommitStatus] =
byCommitStatues(userName, repositoryName, sha).list
def getCommitStatuesWithCreator(userName: String, repositoryName: String, sha: String)(implicit s: Session) :List[(CommitStatus, Account)] =
byCommitStatues(userName, repositoryName, sha).innerJoin(Accounts)
.filter{ case (t,a) => t.creator === a.userName }.list
protected def byCommitStatues(userName: String, repositoryName: String, sha: String)(implicit s: Session) =
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha) ).sortBy(_.updatedDate desc)
}

View File

@@ -1,14 +1,16 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.model._
import gitbucket.core.util.StringUtil._
import gitbucket.core.util.Implicits._
import scala.slick.jdbc.{StaticQuery => Q}
import Q.interpolation
import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile._
import profile.simple._ import profile.simple._
import gitbucket.core.util.StringUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.model._
import scala.slick.jdbc.{StaticQuery => Q}
import Q.interpolation
trait IssuesService { trait IssuesService {
import IssuesService._ import IssuesService._
@@ -20,6 +22,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("comment" , "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 =>
@@ -78,6 +86,47 @@ trait IssuesService {
.toMap .toMap
} }
def getCommitStatues(issueList:Seq[(String, String, Int)])(implicit s: Session) :Map[(String, String, Int), CommitStatusInfo] ={
if(issueList.isEmpty){
Map.empty
}else{
import scala.slick.jdbc._
val issueIdQuery = issueList.map(i => "(PR.USER_NAME=? AND PR.REPOSITORY_NAME=? AND PR.ISSUE_ID=?)").mkString(" OR ")
implicit val qset = SetParameter[Seq[(String, String, Int)]] {
case (seq, pp) =>
for (a <- seq) {
pp.setString(a._1)
pp.setString(a._2)
pp.setInt(a._3)
}
}
import gitbucket.core.model.Profile.commitStateColumnType
val query = Q.query[Seq[(String, String, Int)], (String, String, Int, Int, Int, Option[String], Option[CommitState], Option[String], Option[String])](s"""
SELECT SUMM.USER_NAME, SUMM.REPOSITORY_NAME, SUMM.ISSUE_ID, CS_ALL, CS_SUCCESS
, CSD.CONTEXT, CSD.STATE, CSD.TARGET_URL, CSD.DESCRIPTION
FROM (SELECT
PR.USER_NAME
, PR.REPOSITORY_NAME
, PR.ISSUE_ID
, COUNT(CS.STATE) AS CS_ALL
, SUM(CS.STATE='success') AS CS_SUCCESS
, PR.COMMIT_ID_TO AS COMMIT_ID
FROM PULL_REQUEST PR
JOIN COMMIT_STATUS CS
ON PR.USER_NAME=CS.USER_NAME
AND PR.REPOSITORY_NAME=CS.REPOSITORY_NAME
AND PR.COMMIT_ID_TO=CS.COMMIT_ID
WHERE $issueIdQuery
GROUP BY PR.USER_NAME, PR.REPOSITORY_NAME, PR.ISSUE_ID) as SUMM
LEFT OUTER JOIN COMMIT_STATUS CSD
ON SUMM.CS_ALL = 1 AND SUMM.COMMIT_ID = CSD.COMMIT_ID""");
query(issueList).list.map{
case(userName, repositoryName, issueId, count, successCount, context, state, targetUrl, description) =>
(userName, repositoryName, issueId) -> CommitStatusInfo(count, successCount, context, state, targetUrl, description)
}.toMap
}
}
/** /**
* Returns the search result against issues. * Returns the search result against issues.
* *
@@ -90,8 +139,53 @@ trait IssuesService {
*/ */
def searchIssue(condition: IssueSearchCondition, pullRequest: Boolean, offset: Int, limit: Int, repos: (String, String)*) def searchIssue(condition: IssueSearchCondition, pullRequest: Boolean, offset: Int, limit: Int, repos: (String, String)*)
(implicit s: Session): List[IssueInfo] = { (implicit s: Session): List[IssueInfo] = {
// get issues and comment count and labels // get issues and comment count and labels
val result = 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) }
.map { case ((((t1, t2), t3), t4), t5) =>
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?, t5.title.?)
}
.list
.splitWith { (c1, c2) =>
c1._1.userName == c2._1.userName &&
c1._1.repositoryName == c2._1.repositoryName &&
c1._1.issueId == c2._1.issueId
}
val status = getCommitStatues(result.map(_.head._1).map(is => (is.userName, is.repositoryName, is.issueId)))
result.map { issues => issues.head match {
case (issue, commentCount, _, _, _, milestone) =>
IssueInfo(issue,
issues.flatMap { t => t._3.map (
Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
)} toList,
milestone,
commentCount,
status.get(issue.userName, issue.repositoryName, issue.issueId))
}} 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, Account, Int, PullRequest, Repository, 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) searchIssueQuery(repos, condition, pullRequest)
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
.sortBy { case (t1, t2) => .sortBy { case (t1, t2) =>
@@ -107,28 +201,7 @@ trait IssuesService {
} }
} }
.drop(offset).take(limit) .drop(offset).take(limit)
.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) }
.map { case ((((t1, t2), t3), t4), t5) =>
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?, t5.title.?)
}
.list
.splitWith { (c1, c2) =>
c1._1.userName == c2._1.userName &&
c1._1.repositoryName == c2._1.repositoryName &&
c1._1.issueId == c2._1.issueId
}
.map { issues => issues.head match {
case (issue, commentCount, _, _, _, milestone) =>
IssueInfo(issue,
issues.flatMap { t => t._3.map (
Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
)} toList,
milestone,
commentCount)
}} toList
}
/** /**
* Assembles query for conditional issue searching. * Assembles query for conditional issue searching.
@@ -462,6 +535,8 @@ object IssuesService {
} }
} }
case class IssueInfo(issue: Issue, labels: List[Label], milestone: Option[String], commentCount: Int) case class CommitStatusInfo(count: Int, successCount: Int, context: Option[String], state: Option[CommitState], targetUrl: Option[String], description: Option[String])
case class IssueInfo(issue: Issue, labels: List[Label], milestone: Option[String], commentCount: Int, status:Option[CommitStatusInfo])
} }

View File

@@ -0,0 +1,172 @@
package gitbucket.core.service
import gitbucket.core.model.Account
import gitbucket.core.util.LockUtil
import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.ControlUtil._
import org.eclipse.jgit.merge.MergeStrategy
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.transport.RefSpec
import org.eclipse.jgit.errors.NoMergeBaseException
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
import org.eclipse.jgit.revwalk.RevWalk
trait MergeService {
import MergeService._
/**
* Checks whether conflict will be caused in merging within pull request.
* Returns true if conflict will be caused.
*/
def checkConflict(userName: String, repositoryName: String, branch: String, issueId: Int): Boolean = {
using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
MergeCacheInfo(git, branch, issueId).checkConflict()
}
}
/**
* Checks whether conflict will be caused in merging within pull request.
* only cache check.
* Returns Some(true) if conflict will be caused.
* Returns None if cache has not created yet.
*/
def checkConflictCache(userName: String, repositoryName: String, branch: String, issueId: Int): Option[Boolean] = {
using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
MergeCacheInfo(git, branch, issueId).checkConflictCache()
}
}
/** merge pull request */
def mergePullRequest(git:Git, branch: String, issueId: Int, message:String, committer:PersonIdent): Unit = {
MergeCacheInfo(git, branch, issueId).merge(message, committer)
}
/** fetch remote branch to my repository refs/pull/{issueId}/head */
def fetchAsPullRequest(userName: String, repositoryName: String, requestUserName: String, requestRepositoryName: String, requestBranch:String, issueId:Int){
using(Git.open(getRepositoryDir(userName, repositoryName))){ git =>
git.fetch
.setRemote(getRepositoryDir(requestUserName, requestRepositoryName).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${requestBranch}:refs/pull/${issueId}/head"))
.call
}
}
/**
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
*/
def checkConflict(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
val remoteRefName = s"refs/heads/${branch}"
val tmpRefName = s"refs/merge-check/${userName}/${branch}"
val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true)
try {
// fetch objects from origin repository branch
git.fetch
.setRemote(getRepositoryDir(userName, repositoryName).toURI.toString)
.setRefSpecs(refSpec)
.call
// merge conflict check
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${requestBranch}")
val mergeTip = git.getRepository.resolve(tmpRefName)
try {
!merger.merge(mergeBaseTip, mergeTip)
} catch {
case e: NoMergeBaseException => true
}
} finally {
val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
refUpdate.setForceUpdate(true)
refUpdate.delete()
}
}
}
}
object MergeService{
case class MergeCacheInfo(git:Git, branch:String, issueId:Int){
val repository = git.getRepository
val mergedBranchName = s"refs/pull/${issueId}/merge"
val conflictedBranchName = s"refs/pull/${issueId}/conflict"
lazy val mergeBaseTip = repository.resolve(s"refs/heads/${branch}")
lazy val mergeTip = repository.resolve(s"refs/pull/${issueId}/head")
def checkConflictCache(): Option[Boolean] = {
Option(repository.resolve(mergedBranchName)).flatMap{ merged =>
if(parseCommit( merged ).getParents().toSet == Set( mergeBaseTip, mergeTip )){
// merged branch exists
Some(false)
}else{
None
}
}.orElse(Option(repository.resolve(conflictedBranchName)).flatMap{ conflicted =>
if(parseCommit( conflicted ).getParents().toSet == Set( mergeBaseTip, mergeTip )){
// conflict branch exists
Some(true)
}else{
None
}
})
}
def checkConflict():Boolean ={
checkConflictCache.getOrElse(checkConflictForce)
}
def checkConflictForce():Boolean ={
val merger = MergeStrategy.RECURSIVE.newMerger(repository, true)
val conflicted = try {
!merger.merge(mergeBaseTip, mergeTip)
} catch {
case e: NoMergeBaseException => true
}
val mergeTipCommit = using(new RevWalk( repository ))(_.parseCommit( mergeTip ))
val committer = mergeTipCommit.getCommitterIdent;
def updateBranch(treeId:ObjectId, message:String, branchName:String){
// creates merge commit
val mergeCommitId = createMergeCommit(treeId, committer, message)
// update refs
val refUpdate = repository.updateRef(branchName)
refUpdate.setNewObjectId(mergeCommitId)
refUpdate.setForceUpdate(true)
refUpdate.setRefLogIdent(committer)
refUpdate.update()
}
if(!conflicted){
updateBranch(merger.getResultTreeId, s"Merge ${mergeTip.name} into ${mergeBaseTip.name}", mergedBranchName)
git.branchDelete().setForce(true).setBranchNames(conflictedBranchName).call()
}else{
updateBranch(mergeTipCommit.getTree().getId(), s"can't merge ${mergeTip.name} into ${mergeBaseTip.name}", conflictedBranchName)
git.branchDelete().setForce(true).setBranchNames(mergedBranchName).call()
}
conflicted
}
// update branch from cache
def merge(message:String, committer:PersonIdent) = {
if(checkConflict()){
throw new RuntimeException("This pull request can't merge automatically.")
}
val mergeResultCommit = parseCommit( Option(repository.resolve(mergedBranchName)).getOrElse(throw new RuntimeException(s"not found branch ${mergedBranchName}")) )
// creates merge commit
val mergeCommitId = createMergeCommit(mergeResultCommit.getTree().getId(), committer, message)
// update refs
val refUpdate = repository.updateRef(s"refs/heads/${branch}")
refUpdate.setNewObjectId(mergeCommitId)
refUpdate.setForceUpdate(false)
refUpdate.setRefLogIdent(committer)
refUpdate.setRefLogMessage("merged", true)
refUpdate.update()
}
// return treeId
private def createMergeCommit(treeId:ObjectId, committer:PersonIdent, message:String) = {
val mergeCommit = new CommitBuilder()
mergeCommit.setTreeId(treeId)
mergeCommit.setParentIds(Array[ObjectId](mergeBaseTip, mergeTip): _*)
mergeCommit.setAuthor(committer)
mergeCommit.setCommitter(committer)
mergeCommit.setMessage(message)
// insertObject and got mergeCommit Object Id
val inserter = repository.newObjectInserter
val mergeCommitId = inserter.insert(mergeCommit)
inserter.flush()
inserter.release()
mergeCommitId
}
private def parseCommit(id:ObjectId) = using(new RevWalk( repository ))(_.parseCommit(id))
}
}

View File

@@ -1,10 +1,11 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.model.{Issue, PullRequest} import gitbucket.core.model.{Account, Issue, PullRequest, WebHook}
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._
trait PullRequestService { self: IssuesService => trait PullRequestService { self: IssuesService =>
import PullRequestService._ import PullRequestService._

View File

@@ -55,6 +55,7 @@ trait RepositoryService { self: AccountService =>
val issueComments = IssueComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list val issueComments = IssueComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val commitComments = CommitComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list val commitComments = CommitComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list
val commitStatuses = CommitStatuses.filter(_.byRepository(oldUserName, oldRepositoryName)).list
val collaborators = Collaborators .filter(_.byRepository(oldUserName, oldRepositoryName)).list val collaborators = Collaborators .filter(_.byRepository(oldUserName, oldRepositoryName)).list
Repositories.filter { t => Repositories.filter { t =>
@@ -95,6 +96,7 @@ trait RepositoryService { self: AccountService =>
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
CommitComments.insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) CommitComments.insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
CommitStatuses.insertAll(commitStatuses.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
// Convert labelId // Convert labelId
val oldLabelMap = labels.map(x => (x.labelId, x.labelName)).toMap val oldLabelMap = labels.map(x => (x.labelId, x.labelName)).toMap
@@ -395,5 +397,4 @@ object RepositoryService {
} }
case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode]) case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode])
} }

View File

@@ -1,18 +1,19 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.model.{WebHook, Account} import gitbucket.core.api._
import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment}
import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile._
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.util.JGitUtil
import profile.simple._ import profile.simple._
import org.slf4j.LoggerFactory import gitbucket.core.util.JGitUtil.CommitInfo
import RepositoryService.RepositoryInfo import gitbucket.core.util.RepositoryName
import org.eclipse.jgit.diff.DiffEntry import gitbucket.core.service.RepositoryService.RepositoryInfo
import JGitUtil.CommitInfo
import org.eclipse.jgit.api.Git
import org.apache.http.message.BasicNameValuePair
import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.NameValuePair import org.apache.http.NameValuePair
import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.message.BasicNameValuePair
import org.eclipse.jgit.api.Git
import org.slf4j.LoggerFactory
trait WebHookService { trait WebHookService {
import WebHookService._ import WebHookService._
@@ -28,26 +29,28 @@ trait WebHookService {
def deleteWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit = def deleteWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit =
WebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete WebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete
def callWebHook(owner: String, repository: String, webHookURLs: List[WebHook], payload: WebHookPayload): Unit = { def callWebHookOf(owner: String, repository: String, eventName: String)(makePayload: => Option[WebHookPayload])(implicit s: Session, c: JsonFormat.Context): Unit = {
import org.json4s._ val webHookURLs = getWebHookURLs(owner, repository)
import org.json4s.jackson.Serialization if(webHookURLs.nonEmpty){
import org.json4s.jackson.Serialization.{read, write} makePayload.map(callWebHook(eventName, webHookURLs, _))
}
}
def callWebHook(eventName: String, webHookURLs: List[WebHook], payload: WebHookPayload)(implicit c: JsonFormat.Context): Unit = {
import org.apache.http.client.methods.HttpPost import org.apache.http.client.methods.HttpPost
import org.apache.http.impl.client.HttpClientBuilder import org.apache.http.impl.client.HttpClientBuilder
import scala.concurrent._ import scala.concurrent._
import ExecutionContext.Implicits.global import ExecutionContext.Implicits.global
logger.debug("start callWebHook")
implicit val formats = Serialization.formats(NoTypeHints)
if(webHookURLs.nonEmpty){ if(webHookURLs.nonEmpty){
val json = write(payload) val json = JsonFormat(payload)
val httpClient = HttpClientBuilder.create.build val httpClient = HttpClientBuilder.create.build
webHookURLs.foreach { webHookUrl => webHookURLs.foreach { webHookUrl =>
val f = Future { val f = Future {
logger.debug(s"start web hook invocation for ${webHookUrl}") logger.debug(s"start web hook invocation for ${webHookUrl}")
val httpPost = new HttpPost(webHookUrl.url) val httpPost = new HttpPost(webHookUrl.url)
httpPost.addHeader("X-Github-Event", eventName)
val params: java.util.List[NameValuePair] = new java.util.ArrayList() val params: java.util.List[NameValuePair] = new java.util.ArrayList()
params.add(new BasicNameValuePair("payload", json)) params.add(new BasicNameValuePair("payload", json))
@@ -67,78 +70,203 @@ trait WebHookService {
} }
logger.debug("end callWebHook") logger.debug("end callWebHook")
} }
}
trait WebHookPullRequestService extends WebHookService {
self: AccountService with RepositoryService with PullRequestService with IssuesService =>
import WebHookService._
// https://developer.github.com/v3/activity/events/types/#issuesevent
def callIssuesWebHook(action: String, repository: RepositoryService.RepositoryInfo, issue: Issue, baseUrl: String, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = {
callWebHookOf(repository.owner, repository.name, "issues"){
val users = getAccountsByUserNames(Set(repository.owner, issue.userName), Set(sender))
for{
repoOwner <- users.get(repository.owner)
issueUser <- users.get(issue.userName)
} yield {
WebHookIssuesPayload(
action = action,
number = issue.issueId,
repository = ApiRepository(repository, ApiUser(repoOwner)),
issue = ApiIssue(issue, ApiUser(issueUser)),
sender = ApiUser(sender))
}
}
}
def callPullRequestWebHook(action: String, repository: RepositoryService.RepositoryInfo, issueId: Int, baseUrl: String, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = {
import WebHookService._
callWebHookOf(repository.owner, repository.name, "pull_request"){
for{
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName), Set(sender))
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl)
} yield {
WebHookPullRequestPayload(
action = action,
issue = issue,
pullRequest = pullRequest,
headRepository = headRepo,
headOwner = headOwner,
baseRepository = repository,
baseOwner = baseOwner,
sender = sender)
}
}
}
def getPullRequestsByRequestForWebhook(userName:String, repositoryName:String, branch:String)
(implicit s: Session): Map[(Issue, PullRequest, Account, Account), List[WebHook]] =
(for{
is <- Issues if is.closed === false.bind
pr <- PullRequests if pr.byPrimaryKey(is.userName, is.repositoryName, is.issueId)
if pr.requestUserName === userName.bind
if pr.requestRepositoryName === repositoryName.bind
if pr.requestBranch === branch.bind
bu <- Accounts if bu.userName === pr.userName
ru <- Accounts if ru.userName === pr.requestUserName
wh <- WebHooks if wh.byRepository(is.userName , is.repositoryName)
} yield {
((is, pr, bu, ru), wh)
}).list.groupBy(_._1).mapValues(_.map(_._2))
def callPullRequestWebHookByRequestBranch(action: String, requestRepository: RepositoryService.RepositoryInfo, requestBranch: String, baseUrl: String, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = {
import WebHookService._
for{
((issue, pullRequest, baseOwner, headOwner), webHooks) <- getPullRequestsByRequestForWebhook(requestRepository.owner, requestRepository.name, requestBranch)
baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName, baseUrl)
} yield {
val payload = WebHookPullRequestPayload(
action = action,
issue = issue,
pullRequest = pullRequest,
headRepository = requestRepository,
headOwner = headOwner,
baseRepository = baseRepo,
baseOwner = baseOwner,
sender = sender)
callWebHook("pull_request", webHooks, payload)
}
}
}
trait WebHookIssueCommentService extends WebHookPullRequestService {
self: AccountService with RepositoryService with PullRequestService with IssuesService =>
import WebHookService._
def callIssueCommentWebHook(repository: RepositoryService.RepositoryInfo, issue: Issue, issueCommentId: Int, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = {
callWebHookOf(repository.owner, repository.name, "issue_comment"){
for{
issueComment <- getComment(repository.owner, repository.name, issueCommentId.toString())
users = getAccountsByUserNames(Set(issue.userName, repository.owner, issueComment.userName), Set(sender))
issueUser <- users.get(issue.userName)
repoOwner <- users.get(repository.owner)
commenter <- users.get(issueComment.userName)
} yield {
WebHookIssueCommentPayload(
issue = issue,
issueUser = issueUser,
comment = issueComment,
commentUser = commenter,
repository = repository,
repositoryUser = repoOwner,
sender = sender)
}
}
}
} }
object WebHookService { object WebHookService {
trait WebHookPayload
case class WebHookPayload( // https://developer.github.com/v3/activity/events/types/#pushevent
pusher: WebHookUser, case class WebHookPushPayload(
pusher: ApiUser,
ref: String, ref: String,
commits: List[WebHookCommit], commits: List[ApiCommit],
repository: WebHookRepository) repository: ApiRepository
) extends WebHookPayload
object WebHookPayload { object WebHookPushPayload {
def apply(git: Git, pusher: Account, refName: String, repositoryInfo: RepositoryInfo, def apply(git: Git, pusher: Account, refName: String, repositoryInfo: RepositoryInfo,
commits: List[CommitInfo], repositoryOwner: Account): WebHookPayload = commits: List[CommitInfo], repositoryOwner: Account): WebHookPushPayload =
WebHookPayload( WebHookPushPayload(
WebHookUser(pusher.fullName, pusher.mailAddress), ApiUser(pusher),
refName, refName,
commits.map { commit => commits.map{ commit => ApiCommit(git, RepositoryName(repositoryInfo), commit) },
val diffs = JGitUtil.getDiffs(git, commit.id, false) ApiRepository(
val commitUrl = repositoryInfo.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/commit/" + commit.id repositoryInfo,
owner= ApiUser(repositoryOwner)
WebHookCommit(
id = commit.id,
message = commit.fullMessage,
timestamp = commit.commitTime.toString,
url = commitUrl,
added = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.ADD) => x.newPath },
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 = WebHookUser(
name = commit.committerName,
email = commit.committerEmailAddress
)
)
},
WebHookRepository(
name = repositoryInfo.name,
url = repositoryInfo.httpUrl,
description = repositoryInfo.repository.description.getOrElse(""),
watchers = 0,
forks = repositoryInfo.forkedCount,
`private` = repositoryInfo.repository.isPrivate,
owner = WebHookUser(
name = repositoryOwner.userName,
email = repositoryOwner.mailAddress
)
) )
) )
} }
case class WebHookCommit( // https://developer.github.com/v3/activity/events/types/#issuesevent
id: String, case class WebHookIssuesPayload(
message: String, action: String,
timestamp: String, number: Int,
url: String, repository: ApiRepository,
added: List[String], issue: ApiIssue,
removed: List[String], sender: ApiUser) extends WebHookPayload
modified: List[String],
author: WebHookUser)
case class WebHookRepository( // https://developer.github.com/v3/activity/events/types/#pullrequestevent
name: String, case class WebHookPullRequestPayload(
url: String, action: String,
description: String, number: Int,
watchers: Int, repository: ApiRepository,
forks: Int, pull_request: ApiPullRequest,
`private`: Boolean, sender: ApiUser
owner: WebHookUser) ) extends WebHookPayload
case class WebHookUser(
name: String,
email: String)
object WebHookPullRequestPayload{
def apply(action: String,
issue: Issue,
pullRequest: PullRequest,
headRepository: RepositoryInfo,
headOwner: Account,
baseRepository: RepositoryInfo,
baseOwner: Account,
sender: Account): WebHookPullRequestPayload = {
val headRepoPayload = ApiRepository(headRepository, headOwner)
val baseRepoPayload = ApiRepository(baseRepository, baseOwner)
val senderPayload = ApiUser(sender)
val pr = ApiPullRequest(issue, pullRequest, headRepoPayload, baseRepoPayload, senderPayload)
WebHookPullRequestPayload(
action = action,
number = issue.issueId,
repository = pr.base.repo,
pull_request = pr,
sender = senderPayload
)
}
}
// https://developer.github.com/v3/activity/events/types/#issuecommentevent
case class WebHookIssueCommentPayload(
action: String,
repository: ApiRepository,
issue: ApiIssue,
comment: ApiComment,
sender: ApiUser
) extends WebHookPayload
object WebHookIssueCommentPayload{
def apply(
issue: Issue,
issueUser: Account,
comment: IssueComment,
commentUser: Account,
repository: RepositoryInfo,
repositoryUser: Account,
sender: Account): WebHookIssueCommentPayload =
WebHookIssueCommentPayload(
action = "created",
repository = ApiRepository(repository, repositoryUser),
issue = ApiIssue(issue, ApiUser(issueUser)),
comment = ApiComment(comment, ApiUser(commentUser)),
sender = ApiUser(sender))
}
} }

View File

@@ -0,0 +1,43 @@
package gitbucket.core.servlet
import javax.servlet._
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import gitbucket.core.model.Account
import gitbucket.core.service.AccessTokenService
import gitbucket.core.util.Keys
import org.scalatra.servlet.ServletApiImplicits._
import org.scalatra._
class AccessTokenAuthenticationFilter extends Filter with AccessTokenService {
private val tokenHeaderPrefix = "token "
override def init(filterConfig: FilterConfig): Unit = {}
override def destroy(): Unit = {}
override def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
implicit val request = req.asInstanceOf[HttpServletRequest]
implicit val session = req.getAttribute(Keys.Request.DBSession).asInstanceOf[slick.jdbc.JdbcBackend#Session]
val response = res.asInstanceOf[HttpServletResponse]
Option(request.getHeader("Authorization")).map{
case auth if auth.startsWith("token ") => AccessTokenService.getAccountByAccessToken(auth.substring(6).trim).toRight(Unit)
// TODO Basic Authentication Support
case _ => Left(Unit)
}.orElse{
Option(request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]).map(Right(_))
} match {
case Some(Right(account)) => request.setAttribute(Keys.Session.LoginAccount, account); chain.doFilter(req, res)
case None => chain.doFilter(req, res)
case Some(Left(_)) => {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED)
response.setContentType("Content-Type: application/json; charset=utf-8")
val w = response.getWriter()
w.print("""{ "message": "Bad credentials" }""")
w.close()
}
}
}
}

View File

@@ -1,8 +1,16 @@
package gitbucket.core.servlet package gitbucket.core.servlet
import gitbucket.core.api
import gitbucket.core.model.Session import gitbucket.core.model.Session
import gitbucket.core.service.IssuesService.IssueSearchCondition
import gitbucket.core.service.WebHookService._
import gitbucket.core.service._ import gitbucket.core.service._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util._ import gitbucket.core.util._
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.http.server.GitServlet import org.eclipse.jgit.http.server.GitServlet
import org.eclipse.jgit.lib._ import org.eclipse.jgit.lib._
import org.eclipse.jgit.transport._ import org.eclipse.jgit.transport._
@@ -12,13 +20,7 @@ import org.slf4j.LoggerFactory
import javax.servlet.ServletConfig import javax.servlet.ServletConfig
import javax.servlet.ServletContext import javax.servlet.ServletContext
import javax.servlet.http.{HttpServletResponse, HttpServletRequest} import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import gitbucket.core.util.StringUtil
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import WebHookService._
import org.eclipse.jgit.api.Git
import JGitUtil.CommitInfo
import IssuesService.IssueSearchCondition
/** /**
* Provides Git repository via HTTP. * Provides Git repository via HTTP.
@@ -98,7 +100,8 @@ import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)(implicit session: Session) class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)(implicit session: Session)
extends PostReceiveHook with PreReceiveHook extends PostReceiveHook with PreReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService { with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService
with WebHookPullRequestService {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook]) private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
private var existIds: Seq[String] = Nil private var existIds: Seq[String] = Nil
@@ -122,6 +125,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
val pushedIds = scala.collection.mutable.Set[String]() val pushedIds = scala.collection.mutable.Set[String]()
commands.asScala.foreach { command => commands.asScala.foreach { command =>
logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}") logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
implicit val apiContext = api.JsonFormat.Context(baseUrl)
val refName = command.getRefName.split("/") val refName = command.getRefName.split("/")
val branchName = refName.drop(2).mkString("/") val branchName = refName.drop(2).mkString("/")
val commits = if (refName(1) == "tags") { val commits = if (refName(1) == "tags") {
@@ -138,8 +142,10 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) + countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) +
countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository) countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository)
val repositoryInfo = getRepository(owner, repository, baseUrl).get
// Extract new commit and apply issue comment // Extract new commit and apply issue comment
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch val defaultBranch = repositoryInfo.repository.defaultBranch
val newCommits = commits.flatMap { commit => val newCommits = commits.flatMap { commit =>
if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) { if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) {
if (issueCount > 0) { if (issueCount > 0) {
@@ -176,20 +182,19 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
ReceiveCommand.Type.UPDATE | ReceiveCommand.Type.UPDATE |
ReceiveCommand.Type.UPDATE_NONFASTFORWARD => ReceiveCommand.Type.UPDATE_NONFASTFORWARD =>
updatePullRequests(owner, repository, branchName) updatePullRequests(owner, repository, branchName)
getAccountByUserName(pusher).map{ pusherAccount =>
callPullRequestWebHookByRequestBranch("synchronize", repositoryInfo, branchName, baseUrl, pusherAccount)
}
case _ => case _ =>
} }
} }
// call web hook // call web hook
getWebHookURLs(owner, repository) match { callWebHookOf(owner, repository, "push"){
case webHookURLs if(webHookURLs.nonEmpty) =>
for(pusherAccount <- getAccountByUserName(pusher); for(pusherAccount <- getAccountByUserName(pusher);
ownerAccount <- getAccountByUserName(owner); ownerAccount <- getAccountByUserName(owner)) yield {
repositoryInfo <- getRepository(owner, repository, baseUrl)){ WebHookPushPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount)
callWebHook(owner, repository, webHookURLs,
WebHookPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount))
} }
case _ =>
} }
} }
} }

View File

@@ -21,6 +21,7 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version. * The history of versions. A head of this sequence is the current BitBucket version.
*/ */
val versions = Seq( val versions = Seq(
new Version(3, 1),
new Version(3, 0), new Version(3, 0),
new Version(2, 8), new Version(2, 8),
new Version(2, 7) { new Version(2, 7) {

View File

@@ -1,11 +1,16 @@
package gitbucket.core.util package gitbucket.core.util
import gitbucket.core.api.JsonFormat
import gitbucket.core.controller.Context
import gitbucket.core.servlet.Database import gitbucket.core.servlet.Database
import javax.servlet.http.{HttpSession, HttpServletRequest}
import scala.util.matching.Regex import scala.util.matching.Regex
import scala.util.control.Exception._ import scala.util.control.Exception._
import slick.jdbc.JdbcBackend import slick.jdbc.JdbcBackend
import javax.servlet.http.{HttpSession, HttpServletRequest}
/** /**
* Provides some usable implicit conversions. * Provides some usable implicit conversions.
@@ -15,6 +20,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 context2ApiJsonFormatContext(implicit context: Context): JsonFormat.Context = JsonFormat.Context(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)
@@ -56,7 +63,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

View File

@@ -749,4 +749,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(_))
}
}
} }

View File

@@ -71,6 +71,11 @@ object Keys {
*/ */
val Ajax = "AJAX" val Ajax = "AJAX"
/**
* Request key for the /api/v3 request flag.
*/
val APIv3 = "APIv3"
/** /**
* Request key for the username which is used during Git repository access. * Request key for the username which is used during Git repository access.
*/ */

View File

@@ -0,0 +1,18 @@
package gitbucket.core.util
case class RepositoryName(owner:String, name:String){
val fullName = s"${owner}/${name}"
}
object RepositoryName{
def apply(fullName: String): RepositoryName = {
fullName.split("/").toList match {
case owner :: name :: Nil => RepositoryName(owner, name)
case _ => throw new IllegalArgumentException(s"${fullName} is not repositoryName (only 'owner/name')")
}
}
def apply(repository: gitbucket.core.model.Repository): RepositoryName = RepositoryName(repository.userName, repository.repositoryName)
def apply(repository: gitbucket.core.util.JGitUtil.RepositoryInfo): RepositoryName = RepositoryName(repository.owner, repository.name)
def apply(repository: gitbucket.core.service.RepositoryService.RepositoryInfo): RepositoryName = RepositoryName(repository.owner, repository.name)
def apply(repository: gitbucket.core.model.CommitStatus): RepositoryName = RepositoryName(repository.userName, repository.repositoryName)
}

View File

@@ -4,10 +4,15 @@ import java.text.SimpleDateFormat
import java.util.{Date, Locale, TimeZone} import java.util.{Date, Locale, TimeZone}
import gitbucket.core.controller.Context import gitbucket.core.controller.Context
import gitbucket.core.model.CommitState
import gitbucket.core.service.{RepositoryService, RequestCache} import gitbucket.core.service.{RepositoryService, RequestCache}
import gitbucket.core.util.{JGitUtil, StringUtil} import gitbucket.core.util.{JGitUtil, StringUtil}
import play.twirl.api.Html import play.twirl.api.Html
/** /**
* Provides helper methods for Twirl templates. * Provides helper methods for Twirl templates.
*/ */
@@ -263,4 +268,17 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
def mkHtml(separator: scala.xml.Elem) = Html(seq.mkString(separator.toString)) def mkHtml(separator: scala.xml.Elem) = Html(seq.mkString(separator.toString))
} }
def commitStateIcon(state: CommitState) = Html(state match {
case CommitState.PENDING => "●"
case CommitState.SUCCESS => "&#x2714;"
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"
}
} }

View File

@@ -0,0 +1,57 @@
@(account: gitbucket.core.model.Account,
personalTokens: List[gitbucket.core.model.AccessToken],
gneratedToken: Option[(gitbucket.core.model.AccessToken, String)])(implicit context: gitbucket.core.controller.Context)
@import context._
@import gitbucket.core.view.helpers._
@html.main("Applications"){
<div class="container">
<div class="row-fluid">
<div class="span3">
@menu("application", settings.ssh)
</div>
<div class="span9">
<div class="box">
<div class="box-header">Personal access tokens</div>
<div class="box-content">
@if(personalTokens.isEmpty && gneratedToken.isEmpty){
No tokens.
}else{
Tokens you have generated that can be used to access the GitBucket API.<hr>
}
@gneratedToken.map{ case (token, tokenString) =>
<div class="alert alert-info">
Make sure to copy your new personal access token now. You won't be able to see it again!
</div>
@helper.html.copy("generated-token-copy", tokenString){
<input type="text" value="@tokenString" style="width:21em" readonly>
}
<a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-mini btn-danger pull-right">Delete</a>
<hr>
}
@personalTokens.zipWithIndex.map { case (token, i) =>
@if(i != 0){
<hr>
}
<strong>@token.note</strong>
<a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-mini btn-danger pull-right">Delete</a>
}
</div>
</div>
<form method="POST" action="@path/@account.userName/_personalToken" validate="true">
<div class="box">
<div class="box-header">Generate new token</div>
<div class="box-content">
<fieldset>
<label for="note" class="strong">Token description</label>
<div><span id="error-note" class="error"></span></div>
<input type="text" name="note" id="note" style="width: 400px;"/>
<p class="muted">What's this token for?</p>
</fieldset>
<input type="submit" class="btn btn-success" value="Generate token"/>
</div>
</div>
</form>
</div>
</div>
</div>
}

View File

@@ -10,5 +10,8 @@
<a href="@path/@loginAccount.get.userName/_ssh">SSH Keys</a> <a href="@path/@loginAccount.get.userName/_ssh">SSH Keys</a>
</li> </li>
} }
<li@if(active=="application"){ class="active"}>
<a href="@path/@loginAccount.get.userName/_application">Applications</a>
</li>
</ul> </ul>
</div> </div>

View File

@@ -15,7 +15,7 @@
@dashboard.html.header(openCount, closedCount, condition, groups) @dashboard.html.header(openCount, closedCount, condition, groups)
</th> </th>
</tr> </tr>
@issues.map { case IssueInfo(issue, labels, milestone, commentCount) => @issues.map { case IssueInfo(issue, labels, milestone, commentCount, commitStatus) =>
<tr> <tr>
<td style="padding-top: 15px; padding-bottom: 15px;"> <td style="padding-top: 15px; padding-bottom: 15px;">
@if(issue.isPullRequest){ @if(issue.isPullRequest){
@@ -29,6 +29,7 @@
} else { } else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a> <a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a>
} }
@gitbucket.core.issues.html.commitstatus(issue, commitStatus)
@labels.map { label => @labels.map { label =>
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span> <span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
} }

View File

@@ -0,0 +1,19 @@
@(issue: gitbucket.core.model.Issue, statusInfo: Option[gitbucket.core.service.IssuesService.CommitStatusInfo])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers._
@statusInfo.map{ status =>
@if(status.count==1 && status.state.isDefined){
@if(status.targetUrl.isDefined){
<a href="@status.targetUrl.get" class="text-@status.state.get.name" data-toggle="tooltip" title="@status.state.get.name : @status.description.getOrElse(status.context.get)">@commitStateIcon(status.state.get)</a>
}else{
<span class="text-@status.state.get.name">@commitStateIcon(status.state.get)
}
}else{
@defining(status.count==status.successCount){ isSuccess =>
<a href="@context.path/@issue.userName/@issue.repositoryName/@issue.issueId" class="@if(isSuccess){ text-success }else{ text-error }" data-toggle="tooltip" title="@status.successCount / @status.count checks OK">@if(isSuccess){
&#x2714;
}else{
×
}</a>
}
}
}

View File

@@ -11,12 +11,11 @@
hasWritePermission: Boolean = false)(implicit context: gitbucket.core.controller.Context) hasWritePermission: Boolean = false)(implicit context: gitbucket.core.controller.Context)
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@import gitbucket.core.service.IssuesService
@import gitbucket.core.service.IssuesService.IssueInfo @import gitbucket.core.service.IssuesService.IssueInfo
<br> <br>
@if(condition.nonEmpty){ @if(condition.nonEmpty){
<div> <div>
<a href="@IssuesService.IssueSearchCondition().toURL" class="header-link"> <a href="@gitbucket.core.service.IssuesService.IssueSearchCondition().toURL" class="header-link">
<img src="@assets/common/images/clear.png" class="header-icon"/> <img src="@assets/common/images/clear.png" class="header-icon"/>
<img src="@assets/common/images/clear_hover.png" class="header-icon-hover" style="display: none;"/> <img src="@assets/common/images/clear_hover.png" class="header-icon-hover" style="display: none;"/>
<span class="strong">Clear current search query, filters, and sorts</span> <span class="strong">Clear current search query, filters, and sorts</span>
@@ -26,10 +25,8 @@
<table class="table table-bordered table-hover table-issues"> <table class="table table-bordered table-hover table-issues">
<tr> <tr>
<th style="background-color: #eee;"> <th style="background-color: #eee;">
@if(hasWritePermission){
<input type="checkbox"/> <input type="checkbox"/>
} <span class="small">
<span class="small" id="table-issues-control">
<a class="button-link@if(condition.state == "open"){ selected}" href="@condition.copy(state = "open").toURL"> <a class="button-link@if(condition.state == "open"){ selected}" href="@condition.copy(state = "open").toURL">
<img src="@assets/common/images/status-open@(if(condition.state == "open"){"-active"}).png"/> <img src="@assets/common/images/status-open@(if(condition.state == "open"){"-active"}).png"/>
@openCount Open @openCount Open
@@ -38,7 +35,8 @@
<img src="@assets/common/images/status-closed@(if(condition.state == "closed"){"-active"}).png"/> <img src="@assets/common/images/status-closed@(if(condition.state == "closed"){"-active"}).png"/>
@closedCount Closed @closedCount Closed
</a> </a>
<div class="pull-right"> </span>
<div class="pull-right" id="table-issues-control">
@helper.html.dropdown("Author", flat = true) { @helper.html.dropdown("Author", flat = true) {
@collaborators.map { collaborator => @collaborators.map { collaborator =>
<li> <li>
@@ -117,11 +115,8 @@
</li> </li>
} }
</div> </div>
</span>
@if(hasWritePermission){ @if(hasWritePermission){
<span class="small" id="table-issues-batchedit"> <div class="pull-right" id="table-issues-batchedit">
<span id="batchedit-selected"></span> selected
<div class="pull-right">
@helper.html.dropdown("Mark as", flat = true) { @helper.html.dropdown("Mark as", flat = true) {
<li><a href="javascript:void(0);" class="toggle-state" data-id="open">Open</a></li> <li><a href="javascript:void(0);" class="toggle-state" data-id="open">Open</a></li>
<li><a href="javascript:void(0);" class="toggle-state" data-id="close">Close</a></li> <li><a href="javascript:void(0);" class="toggle-state" data-id="close">Close</a></li>
@@ -143,14 +138,13 @@
<li><a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId">@milestone.title</a></li> <li><a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId">@milestone.title</a></li>
} }
} }
@helper.html.dropdown("Assign", flat = true) { @helper.html.dropdown("Assignee", flat = true) {
<li><a href="javascript:void(0);" class="toggle-assign" data-name="">Assign to nobody</a></li> <li><a href="javascript:void(0);" class="toggle-assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
@collaborators.map { collaborator => @collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li> <li><a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li>
} }
} }
</div> </div>
</span>
} }
</th> </th>
</tr> </tr>
@@ -176,7 +170,7 @@
</td> </td>
</tr> </tr>
} }
@issues.map { case IssueInfo(issue, labels, milestone, commentCount) => @issues.map { case IssueInfo(issue, labels, milestone, commentCount, commitStatus) =>
<tr> <tr>
<td style="padding-top: 15px; padding-bottom: 15px;"> <td style="padding-top: 15px; padding-bottom: 15px;">
@if(hasWritePermission){ @if(hasWritePermission){
@@ -191,6 +185,7 @@
} else { } else {
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a> <a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
} }
@commitstatus(issue, commitStatus)
@labels.map { label => @labels.map { label =>
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span> <span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
} }
@@ -219,6 +214,6 @@
} }
</table> </table>
<div class="pull-right"> <div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), IssuesService.IssueLimit, 10, condition.toURL) @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), gitbucket.core.service.IssuesService.IssueLimit, 10, condition.toURL)
</div> </div>

View File

@@ -10,37 +10,23 @@
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@import gitbucket.core.model._ @import gitbucket.core.model._
<div class="row-fluid"> <div class="row-fluid">
<div class="span10"> <div class="span10">
<div id="comment-list"> <div id="comment-list">
@issues.html.commentlist(Some(issue), comments, hasWritePermission, repository, Some(pullreq)) @issues.html.commentlist(Some(issue), comments, hasWritePermission, repository, Some(pullreq))
</div> </div>
@defining(comments.flatMap { @defining(comments.flatMap {
case comment: IssueComment => Some(comment) case comment: gitbucket.core.model.IssueComment => Some(comment)
case other => None case other => None
}.exists(_.action == "merge")){ merged => }.exists(_.action == "merge")){ merged =>
@if(hasWritePermission && !issue.closed){ @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;"> <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... <img src="@assets/common/images/indicator.gif"/> Checking...
</div> </div>
</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>
</div>
</div> </div>
} }
@if(hasWritePermission && issue.closed && pullreq.userName == pullreq.requestUserName && merged && @if(hasWritePermission && issue.closed && pullreq.userName == pullreq.requestUserName && merged &&

View File

@@ -1,17 +1,60 @@
@(hasConflict: Boolean, @(hasConflict: Boolean,
hasProblem: Boolean,
issue: gitbucket.core.model.Issue,
pullreq: gitbucket.core.model.PullRequest, pullreq: gitbucket.core.model.PullRequest,
statuses: List[model.CommitStatus],
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
requestRepositoryUrl: String)(implicit context: gitbucket.core.controller.Context) requestRepositoryUrl: String)(implicit context: gitbucket.core.controller.Context)
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.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"> <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>
<div> <div>
@if(hasConflict){ @if(hasConflict){
<span class="strong">We cant automatically merge this pull request.</span> <span class="strong">We cant automatically merge this pull request.</span>
} else{ @if(hasProblem){
<span class="strong">Merge with caution!</span>
} else { } else {
<span class="strong">This pull request can be automatically merged.</span> <span class="strong">This pull request can be automatically merged.</span>
} } }
</div> </div>
<div class="small"> <div class="small">
@if(hasConflict){ @if(hasConflict){
@@ -69,12 +112,38 @@
} }
</div> </div>
</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> <script>
$(function(){ $(function(){
$('#show-command-line').click(function(){ $('#show-command-line').click(function(){
$('#command-line').show(); $('#command-line').show();
return false; 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-button').click(function(){
$('#merge-pull-request').hide(); $('#merge-pull-request').hide();

View File

@@ -1006,6 +1006,24 @@ div.author-info div.committer {
font-size: 12px; 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 */ /* Diff */
/****************************************************************************/ /****************************************************************************/

View File

@@ -0,0 +1,281 @@
package gitbucket.core.api
import gitbucket.core.util.RepositoryName
import org.specs2.mutable.Specification
import org.json4s.jackson.JsonMethods.{pretty, parse}
import org.json4s._
import org.specs2.matcher._
import java.util.{Calendar, TimeZone}
class JsonFormatSpec extends Specification {
val date1 = {
val d = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
d.set(2011,3,14,16,0,49)
d.getTime
}
val sha1 = "6dcb09b5b57875f334f61aebed695e2e4193db5e"
val repo1Name = RepositoryName("octocat/Hello-World")
implicit val context = JsonFormat.Context("http://gitbucket.exmple.com")
val apiUser = ApiUser(
login= "octocat",
email= "octocat@example.com",
`type`= "User",
site_admin= false,
created_at= date1)
val apiUserJson = """{
"login":"octocat",
"email":"octocat@example.com",
"type":"User",
"site_admin":false,
"created_at":"2011-04-14T16:00:49Z",
"url":"http://gitbucket.exmple.com/api/v3/users/octocat",
"html_url":"http://gitbucket.exmple.com/octocat"
}"""
val repository = ApiRepository(
name = repo1Name.name,
full_name = repo1Name.fullName,
description = "This your first repo!",
watchers = 0,
forks = 0,
`private` = false,
default_branch = "master",
owner = apiUser)
val repositoryJson = s"""{
"name" : "Hello-World",
"full_name" : "octocat/Hello-World",
"description" : "This your first repo!",
"watchers" : 0,
"forks" : 0,
"private" : false,
"default_branch" : "master",
"owner" : $apiUserJson,
"forks_count" : 0,
"watchers_coun" : 0,
"url" : "${context.baseUrl}/api/v3/repos/octocat/Hello-World",
"http_url" : "${context.baseUrl}/git/octocat/Hello-World.git",
"clone_url" : "${context.baseUrl}/git/octocat/Hello-World.git",
"html_url" : "${context.baseUrl}/octocat/Hello-World"
}"""
val apiCommitStatus = ApiCommitStatus(
created_at = date1,
updated_at = date1,
state = "success",
target_url = Some("https://ci.example.com/1000/output"),
description = Some("Build has completed successfully"),
id = 1,
context = "Default",
creator = apiUser
)(sha1, repo1Name)
val apiCommitStatusJson = s"""{
"created_at":"2011-04-14T16:00:49Z",
"updated_at":"2011-04-14T16:00:49Z",
"state":"success",
"target_url":"https://ci.example.com/1000/output",
"description":"Build has completed successfully",
"id":1,
"context":"Default",
"creator":$apiUserJson,
"url": "http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/statuses"
}"""
val apiComment = ApiComment(
id =1,
user = apiUser,
body= "Me too",
created_at= date1,
updated_at= date1)
val apiCommentJson = s"""{
"id": 1,
"body": "Me too",
"user": $apiUserJson,
"created_at": "2011-04-14T16:00:49Z",
"updated_at": "2011-04-14T16:00:49Z"
}"""
val apiPersonIdent = ApiPersonIdent("Monalisa Octocat","support@example.com",date1)
val apiPersonIdentJson = """ {
"name": "Monalisa Octocat",
"email": "support@example.com",
"date": "2011-04-14T16:00:49Z"
}"""
val apiCommitListItem = ApiCommitListItem(
sha = sha1,
commit = ApiCommitListItem.Commit(
message = "Fix all the bugs",
author = apiPersonIdent,
committer = apiPersonIdent
)(sha1, repo1Name),
author = Some(apiUser),
committer= Some(apiUser),
parents= Seq(ApiCommitListItem.Parent("6dcb09b5b57875f334f61aebed695e2e4193db5e")(repo1Name)))(repo1Name)
val apiCommitListItemJson = s"""{
"url": "${context.baseUrl}/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e",
"sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
"commit": {
"url": "${context.baseUrl}/api/v3/repos/octocat/Hello-World/git/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e",
"author": $apiPersonIdentJson,
"committer": $apiPersonIdentJson,
"message": "Fix all the bugs"
},
"author": $apiUserJson,
"committer": $apiUserJson,
"parents": [
{
"url": "${context.baseUrl}/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e",
"sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e"
}
]
}"""
val apiCombinedCommitStatus = ApiCombinedCommitStatus(
state = "success",
sha = sha1,
total_count = 2,
statuses = List(apiCommitStatus),
repository = repository)
val apiCombinedCommitStatusJson = s"""{
"state": "success",
"sha": "$sha1",
"total_count": 2,
"statuses": [ $apiCommitStatusJson ],
"repository": $repositoryJson,
"url": "${context.baseUrl}/api/v3/repos/octocat/Hello-World/commits/$sha1/status"
}"""
val apiIssue = ApiIssue(
number = 1347,
title = "Found a bug",
user = apiUser,
state = "open",
body = "I'm having a problem with this.",
created_at = date1,
updated_at = date1)
val apiIssueJson = s"""{
"number": 1347,
"state": "open",
"title": "Found a bug",
"body": "I'm having a problem with this.",
"user": $apiUserJson,
"created_at": "2011-04-14T16:00:49Z",
"updated_at": "2011-04-14T16:00:49Z"
}"""
val apiPullRequest = ApiPullRequest(
number = 1347,
updated_at = date1,
created_at = date1,
head = ApiPullRequest.Commit(
sha = sha1,
ref = "new-topic",
repo = repository)("octocat"),
base = ApiPullRequest.Commit(
sha = sha1,
ref = "master",
repo = repository)("octocat"),
mergeable = None,
title = "new-feature",
body = "Please pull these awesome changes",
user = apiUser
)
val apiPullRequestJson = s"""{
"url": "${context.baseUrl}/api/v3/repos/octocat/Hello-World/pulls/1347",
"html_url": "${context.baseUrl}/octocat/Hello-World/pull/1347",
// "diff_url": "${context.baseUrl}/octocat/Hello-World/pull/1347.diff",
// "patch_url": "${context.baseUrl}/octocat/Hello-World/pull/1347.patch",
// "issue_url": "${context.baseUrl}/api/v3/repos/octocat/Hello-World/issues/1347",
"commits_url": "${context.baseUrl}/api/v3/repos/octocat/Hello-World/pulls/1347/commits",
"review_comments_url": "${context.baseUrl}/api/v3/repos/octocat/Hello-World/pulls/1347/comments",
"review_comment_url": "${context.baseUrl}/api/v3/repos/octocat/Hello-World/pulls/comments/{number}",
"comments_url": "${context.baseUrl}/api/v3/repos/octocat/Hello-World/issues/1347/comments",
"statuses_url": "${context.baseUrl}/api/v3/repos/octocat/Hello-World/statuses/6dcb09b5b57875f334f61aebed695e2e4193db5e",
"number": 1347,
// "state": "open",
"title": "new-feature",
"body": "Please pull these awesome changes",
"created_at": "2011-04-14T16:00:49Z",
"updated_at": "2011-04-14T16:00:49Z",
// "closed_at": "2011-04-14T16:00:49Z",
// "merged_at": "2011-04-14T16:00:49Z",
"head": {
"label": "new-topic",
"ref": "new-topic",
"sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
"user": $apiUserJson,
"repo": $repositoryJson
},
"base": {
"label": "master",
"ref": "master",
"sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
"user": $apiUserJson,
"repo": $repositoryJson
},
"user": $apiUserJson
// "merge_commit_sha": "e5bd3914e2e596debea16f433f57875b5b90bcd6",
// "merged": false,
// "mergeable": true,
// "merged_by": $$apiUserJson,
// "comments": 10,
// "commits": 3,
// "additions": 100,
// "deletions": 3,
// "changed_files": 5
}"""
def beFormatted(json2Arg:String) = new Matcher[String] {
def apply[S <: String](e: Expectable[S]) = {
import java.util.regex.Pattern
val json2 = Pattern.compile("""^\s*//.*$""", Pattern.MULTILINE).matcher(json2Arg).replaceAll("")
val js2 = try{
parse(json2)
}catch{
case e:com.fasterxml.jackson.core.JsonParseException => {
val p = java.lang.Math.max(e.getLocation.getCharOffset()-10,0).toInt
val message = json2.substring(p,java.lang.Math.min(p+100,json2.length))
throw new com.fasterxml.jackson.core.JsonParseException(message + e.getMessage , e.getLocation)
}
}
val js1 = parse(e.value)
result(js1 == js2,
"expected",
{
val diff = js2 diff js1
s"${pretty(js1)} is not ${pretty(js2)} \n\n ${pretty(Extraction.decompose(diff)(org.json4s.DefaultFormats))}"
},
e)
}
}
"JsonFormat" should {
"apiUser" in {
JsonFormat(apiUser) must beFormatted(apiUserJson)
}
"repository" in {
JsonFormat(repository) must beFormatted(repositoryJson)
}
"apiComment" in {
JsonFormat(apiComment) must beFormatted(apiCommentJson)
}
"apiCommitListItem" in {
JsonFormat(apiCommitListItem) must beFormatted(apiCommitListItemJson)
}
"apiCommitStatus" in {
JsonFormat(apiCommitStatus) must beFormatted(apiCommitStatusJson)
}
"apiCombinedCommitStatus" in {
JsonFormat(apiCombinedCommitStatus) must beFormatted(apiCombinedCommitStatusJson)
}
"apiIssue" in {
JsonFormat(apiIssue) must beFormatted(apiIssueJson)
}
"apiPullRequest" in {
JsonFormat(apiPullRequest) must beFormatted(apiPullRequestJson)
}
}
}

View File

@@ -0,0 +1,26 @@
package gitbucket.core.model
import gitbucket.core.model.CommitState._
import org.specs2.mutable.Specification
class CommitStateSpec extends Specification {
"CommitState" should {
"combine empty must eq PENDING" in {
combine(Set()) must_== PENDING
}
"combine includes ERROR must eq FAILURE" in {
combine(Set(ERROR, SUCCESS, PENDING)) must_== FAILURE
}
"combine includes FAILURE must eq peinding" in {
combine(Set(FAILURE, SUCCESS, PENDING)) must_== FAILURE
}
"combine includes PENDING must eq peinding" in {
combine(Set(PENDING, SUCCESS)) must_== PENDING
}
"combine only SUCCESS must eq SUCCESS" in {
combine(Set(SUCCESS)) must_== SUCCESS
}
}
}

View File

@@ -0,0 +1,91 @@
package gitbucket.core.service
import gitbucket.core.model._
import org.specs2.mutable.Specification
import java.util.Date
class AccessTokenServiceSpec extends Specification with ServiceSpecBase {
"AccessTokenService" should {
"generateAccessToken" in { withTestDB { implicit session =>
AccessTokenService.generateAccessToken("root", "note") must be like{
case (id, token) if id != 0 => ok
}
}}
"getAccessTokens" in { withTestDB { implicit session =>
val (id, token) = AccessTokenService.generateAccessToken("root", "note")
val tokenHash = AccessTokenService.tokenToHash(token)
AccessTokenService.getAccessTokens("root") must be like{
case List(AccessToken(`id`, "root", `tokenHash`, "note")) => ok
}
}}
"getAccessTokens(root) get root's tokens" in { withTestDB { implicit session =>
val (id, token) = AccessTokenService.generateAccessToken("root", "note")
val tokenHash = AccessTokenService.tokenToHash(token)
val user2 = generateNewAccount("user2")
AccessTokenService.generateAccessToken("user2", "note2")
AccessTokenService.getAccessTokens("root") must be like{
case List(AccessToken(`id`, "root", `tokenHash`, "note")) => ok
}
}}
"deleteAccessToken" in { withTestDB { implicit session =>
val (id, token) = AccessTokenService.generateAccessToken("root", "note")
val user2 = generateNewAccount("user2")
AccessTokenService.generateAccessToken("user2", "note2")
AccessTokenService.deleteAccessToken("root", id)
AccessTokenService.getAccessTokens("root") must beEmpty
}}
"getAccountByAccessToken" in { withTestDB { implicit session =>
val (id, token) = AccessTokenService.generateAccessToken("root", "note")
AccessTokenService.getAccountByAccessToken(token) must beSome.like {
case user => user.userName must_== "root"
}
}}
"getAccountByAccessToken don't get removed account" in { withTestDB { implicit session =>
val user2 = generateNewAccount("user2")
val (id, token) = AccessTokenService.generateAccessToken("user2", "note")
AccountService.updateAccount(user2.copy(isRemoved=true))
AccessTokenService.getAccountByAccessToken(token) must beEmpty
}}
"generateAccessToken create uniq token" in { withTestDB { implicit session =>
val tokenIt = List("token1","token1","token1","token2").iterator
val service = new AccessTokenService{
override def makeAccessTokenString:String = tokenIt.next
}
service.generateAccessToken("root", "note1") must like{
case (_, "token1") => ok
}
service.generateAccessToken("root", "note2") must like{
case (_, "token2") => ok
}
}}
"when update Account.userName then AccessToken.userName changed" in { withTestDB { implicit session =>
val user2 = generateNewAccount("user2")
val (id, token) = AccessTokenService.generateAccessToken("user2", "note")
import gitbucket.core.model.Profile._
import profile.simple._
Accounts.filter(_.userName === "user2".bind).map(_.userName).update("user3")
AccessTokenService.getAccountByAccessToken(token) must beSome.like {
case user => user.userName must_== "user3"
}
}}
}
}

View File

@@ -0,0 +1,78 @@
package gitbucket.core.service
import gitbucket.core.model._
import gitbucket.core.model.Profile._
import profile.simple._
import org.specs2.mutable.Specification
import java.util.Date
class CommitStatusServiceSpec extends Specification with ServiceSpecBase with CommitStatusService
with RepositoryService with AccountService{
val now = new java.util.Date()
val fixture1 = CommitStatus(
userName = "root",
repositoryName = "repo",
commitId = "0e97b8f59f7cdd709418bb59de53f741fd1c1bd7",
context = "jenkins/test",
creator = "tester",
state = CommitState.PENDING,
targetUrl = Some("http://example.com/target"),
description = Some("description"),
updatedDate = now,
registeredDate = now)
def findById(id: Int)(implicit s:Session) = CommitStatuses.filter(_.byPrimaryKey(id)).firstOption
def generateFixture1(tester:Account)(implicit s:Session) = createCommitStatus(
userName = fixture1.userName,
repositoryName = fixture1.repositoryName,
sha = fixture1.commitId,
context = fixture1.context,
state = fixture1.state,
targetUrl = fixture1.targetUrl,
description = fixture1.description,
creator = tester,
now = fixture1.registeredDate)
"CommitStatusService" should {
"createCommitState can insert and update" in { withTestDB { implicit session =>
val tester = generateNewAccount(fixture1.creator)
createRepository(fixture1.repositoryName,fixture1.userName,None,false)
val id = generateFixture1(tester:Account)
getCommitStatus(fixture1.userName, fixture1.repositoryName, id) must_==
Some(fixture1.copy(commitStatusId=id))
// other one can update
val tester2 = generateNewAccount("tester2")
val time2 = new java.util.Date();
val id2 = createCommitStatus(
userName = fixture1.userName,
repositoryName = fixture1.repositoryName,
sha = fixture1.commitId,
context = fixture1.context,
state = CommitState.SUCCESS,
targetUrl = Some("http://example.com/target2"),
description = Some("description2"),
creator = tester2,
now = time2)
getCommitStatus(fixture1.userName, fixture1.repositoryName, id2) must_== Some(fixture1.copy(
commitStatusId = id,
creator = "tester2",
state = CommitState.SUCCESS,
targetUrl = Some("http://example.com/target2"),
description = Some("description2"),
updatedDate = time2))
}}
"getCommitStatus can find by commitId and context" in { withTestDB { implicit session =>
val tester = generateNewAccount(fixture1.creator)
createRepository(fixture1.repositoryName,fixture1.userName,None,false)
val id = generateFixture1(tester:Account)
getCommitStatus(fixture1.userName, fixture1.repositoryName, fixture1.commitId, fixture1.context) must_== Some(fixture1.copy(commitStatusId=id))
}}
"getCommitStatus can find by commitStatusId" in { withTestDB { implicit session =>
val tester = generateNewAccount(fixture1.creator)
createRepository(fixture1.repositoryName,fixture1.userName,None,false)
val id = generateFixture1(tester:Account)
getCommitStatus(fixture1.userName, fixture1.repositoryName, id) must_== Some(fixture1.copy(commitStatusId=id))
}}
}
}

View File

@@ -0,0 +1,50 @@
package gitbucket.core.service
import gitbucket.core.model._
import gitbucket.core.service.IssuesService._
import org.specs2.mutable.Specification
import java.util.Date
class IssuesServiceSpec extends Specification with ServiceSpecBase {
"IssuesService" should {
"getCommitStatues" in { withTestDB { implicit session =>
val user1 = generateNewUserWithDBRepository("user1","repo1")
def getCommitStatues = dummyService.getCommitStatues(List(("user1","repo1",1),("user1","repo1",2)))
getCommitStatues must_== Map.empty
val now = new java.util.Date()
val issueId = generateNewIssue("user1","repo1")
issueId must_== 1
getCommitStatues must_== Map.empty
val cs = dummyService.createCommitStatus("user1","repo1","shasha", "default", CommitState.SUCCESS, Some("http://exmple.com/ci"), Some("exampleService"), now, user1)
getCommitStatues must_== Map.empty
val (is2, pr2) = generateNewPullRequest("user1/repo1/master","user1/repo1/feature1")
pr2.issueId must_== 2
// if there are no statuses, state is none
getCommitStatues must_== Map.empty
// if there is a status, state is that
val cs2 = dummyService.createCommitStatus("user1","repo1","feature1", "default", CommitState.SUCCESS, Some("http://exmple.com/ci"), Some("exampleService"), now, user1)
getCommitStatues must_== Map(("user1","repo1",2) -> CommitStatusInfo(1,1,Some("default"),Some(CommitState.SUCCESS),Some("http://exmple.com/ci"),Some("exampleService")))
// if there are two statuses, state is none
val cs3 = dummyService.createCommitStatus("user1","repo1","feature1", "pend", CommitState.PENDING, Some("http://exmple.com/ci"), Some("exampleService"), now, user1)
getCommitStatues must_== Map(("user1","repo1",2) -> CommitStatusInfo(2,1,None,None,None,None))
// get only statuses in query issues
val (is3, pr3) = generateNewPullRequest("user1/repo1/master","user1/repo1/feature3")
val cs4 = dummyService.createCommitStatus("user1","repo1","feature3", "none", CommitState.PENDING, None, None, now, user1)
getCommitStatues must_== Map(("user1","repo1",2) -> CommitStatusInfo(2,1,None,None,None,None))
} }
}
}

View File

@@ -0,0 +1,159 @@
package gitbucket.core.service
import gitbucket.core.model._
import gitbucket.core.util.JGitUtil
import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.ControlUtil._
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.dircache.DirCache
import org.eclipse.jgit.lib._
import org.eclipse.jgit.revwalk._
import org.eclipse.jgit.treewalk._
import org.specs2.mutable.Specification
import java.nio.file._
import java.util.Date
class MergeServiceSpec extends Specification {
sequential
val service = new MergeService{}
val branch = "master"
val issueId = 10
def initRepository(owner:String, name:String) = {
val repo1Dir = getRepositoryDir(owner, name)
RepositoryCache.clear()
FileUtils.deleteQuietly(repo1Dir)
Files.createDirectories(repo1Dir.toPath())
JGitUtil.initRepository(repo1Dir)
using(Git.open(repo1Dir)){ git =>
createFile(git, s"refs/heads/master", "test.txt", "hoge" )
git.branchCreate().setStartPoint(s"refs/heads/master").setName(s"refs/pull/${issueId}/head").call()
}
repo1Dir
}
def createFile(git:Git, branch:String, name:String, content:String){
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(branch + "^{commit}")
builder.add(JGitUtil.createDirCacheEntry(name, FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
branch, "dummy", "dummy@example.com", "Initial commit")
}
def getFile(git:Git, branch:String, path:String) = {
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
val objectId = using(new TreeWalk(git.getRepository)){ walk =>
walk.addTree(revCommit.getTree)
walk.setRecursive(true)
@scala.annotation.tailrec
def _getPathObjectId: ObjectId = walk.next match {
case true if(walk.getPathString == path) => walk.getObjectId(0)
case true => _getPathObjectId
case false => throw new Exception(s"not found ${branch} / ${path}")
}
_getPathObjectId
}
JGitUtil.getContentInfo(git, path, objectId)
}
def createConfrict(git:Git) = {
createFile(git, s"refs/heads/${branch}", "test.txt", "hoge2" )
createFile(git, s"refs/pull/${issueId}/head", "test.txt", "hoge4" )
}
"checkConflict, checkConflictCache" should {
"checkConflict false if not conflicted, and create cache" in {
val repo1Dir = initRepository("user1","repo1")
service.checkConflictCache("user1", "repo1", branch, issueId) mustEqual None
val conflicted = service.checkConflict("user1", "repo1", branch, issueId)
service.checkConflictCache("user1", "repo1", branch, issueId) mustEqual Some(false)
conflicted mustEqual false
}
"checkConflict true if not conflicted, and create cache" in {
val repo1Dir = initRepository("user1","repo1")
using(Git.open(repo1Dir)){ git =>
createConfrict(git)
}
service.checkConflictCache("user1", "repo1", branch, issueId) mustEqual None
val conflicted = service.checkConflict("user1", "repo1", branch, issueId)
conflicted mustEqual true
service.checkConflictCache("user1", "repo1", branch, issueId) mustEqual Some(true)
}
}
"checkConflictCache" should {
"merged cache invalid if origin branch moved" in {
val repo1Dir = initRepository("user1","repo1")
service.checkConflict("user1", "repo1", branch, issueId) mustEqual false
service.checkConflictCache("user1", "repo1", branch, issueId) mustEqual Some(false)
using(Git.open(repo1Dir)){ git =>
createFile(git, s"refs/heads/${branch}", "test.txt", "hoge2" )
}
service.checkConflictCache("user1", "repo1", branch, issueId) mustEqual None
}
"merged cache invalid if request branch moved" in {
val repo1Dir = initRepository("user1","repo1")
service.checkConflict("user1", "repo1", branch, issueId) mustEqual false
service.checkConflictCache("user1", "repo1", branch, issueId) mustEqual Some(false)
using(Git.open(repo1Dir)){ git =>
createFile(git, s"refs/pull/${issueId}/head", "test.txt", "hoge4" )
}
service.checkConflictCache("user1", "repo1", branch, issueId) mustEqual None
}
"merged cache invalid if origin branch moved" in {
val repo1Dir = initRepository("user1","repo1")
service.checkConflict("user1", "repo1", branch, issueId) mustEqual false
service.checkConflictCache("user1", "repo1", branch, issueId) mustEqual Some(false)
using(Git.open(repo1Dir)){ git =>
createFile(git, s"refs/heads/${branch}", "test.txt", "hoge2" )
}
service.checkConflictCache("user1", "repo1", branch, issueId) mustEqual None
}
"conflicted cache invalid if request branch moved" in {
val repo1Dir = initRepository("user1","repo1")
using(Git.open(repo1Dir)){ git =>
createConfrict(git)
}
service.checkConflict("user1", "repo1", branch, issueId) mustEqual true
service.checkConflictCache("user1", "repo1", branch, issueId) mustEqual Some(true)
using(Git.open(repo1Dir)){ git =>
createFile(git, s"refs/pull/${issueId}/head", "test.txt", "hoge4" )
}
service.checkConflictCache("user1", "repo1", branch, issueId) mustEqual None
}
"conflicted cache invalid if origin branch moved" in {
val repo1Dir = initRepository("user1","repo1")
using(Git.open(repo1Dir)){ git =>
createConfrict(git)
}
service.checkConflict("user1", "repo1", branch, issueId) mustEqual true
service.checkConflictCache("user1", "repo1", branch, issueId) mustEqual Some(true)
using(Git.open(repo1Dir)){ git =>
createFile(git, s"refs/heads/${branch}", "test.txt", "hoge4" )
}
service.checkConflictCache("user1", "repo1", branch, issueId) mustEqual None
}
}
"mergePullRequest" should {
"can merge" in {
val repo1Dir = initRepository("user1","repo1")
using(Git.open(repo1Dir)){ git =>
createFile(git, s"refs/pull/${issueId}/head", "test.txt", "hoge2" )
val committer = new PersonIdent("dummy2", "dummy2@example.com")
getFile(git, branch, "test.txt").content.get mustEqual "hoge"
val requestBranchId = git.getRepository.resolve(s"refs/pull/${issueId}/head")
val masterId = git.getRepository.resolve(branch)
service.mergePullRequest(git, branch, issueId, "merged", committer)
val lastCommitId = git.getRepository.resolve(branch);
val commit = using(new RevWalk(git.getRepository))(_.parseCommit(lastCommitId))
commit.getCommitterIdent() mustEqual committer
commit.getAuthorIdent() mustEqual committer
commit.getFullMessage() mustEqual "merged"
commit.getParents.toSet mustEqual Set( requestBranchId, masterId )
getFile(git, branch, "test.txt").content.get mustEqual "hoge2"
}
}
}
}

View File

@@ -0,0 +1,35 @@
package gitbucket.core.service
import gitbucket.core.model._
import gitbucket.core.model.Profile._
import org.specs2.mutable.Specification
class RepositoryServiceSpec extends Specification with ServiceSpecBase with RepositoryService with AccountService{
"RepositoryService" should {
"renameRepository can rename CommitState" in { withTestDB { implicit session =>
val tester = generateNewAccount("tester")
createRepository("repo","root",None,false)
val commitStatusService = new CommitStatusService{}
val id = commitStatusService.createCommitStatus(
userName = "root",
repositoryName = "repo",
sha = "0e97b8f59f7cdd709418bb59de53f741fd1c1bd7",
context = "jenkins/test",
state = CommitState.PENDING,
targetUrl = Some("http://example.com/target"),
description = Some("description"),
creator = tester,
now = new java.util.Date)
val org = commitStatusService.getCommitStatus("root","repo", id).get
renameRepository("root","repo","tester","repo2")
val neo = commitStatusService.getCommitStatus("tester","repo2", org.commitId, org.context).get
neo must_==
org.copy(
commitStatusId=neo.commitStatusId,
repositoryName="repo2",
userName="tester")
}}
}
}

View File

@@ -2,14 +2,19 @@ package gitbucket.core.service
import gitbucket.core.servlet.AutoUpdate import gitbucket.core.servlet.AutoUpdate
import gitbucket.core.util.{ControlUtil, DatabaseConfig, FileUtil} import gitbucket.core.util.{ControlUtil, DatabaseConfig, FileUtil}
import gitbucket.core.util.ControlUtil._
import gitbucket.core.model._
import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile._
import profile.simple._ import profile.simple._
import ControlUtil._
import java.sql.DriverManager
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import scala.util.Random
import java.sql.DriverManager
import java.io.File import java.io.File
import scala.util.Random
trait ServiceSpecBase { trait ServiceSpecBase {
def withTestDB[A](action: (Session) => A): A = { def withTestDB[A](action: (Session) => A): A = {
@@ -25,4 +30,46 @@ trait ServiceSpecBase {
} }
} }
def generateNewAccount(name:String)(implicit s:Session):Account = {
AccountService.createAccount(name, name, name, s"${name}@example.com", false, None)
AccountService.getAccountByUserName(name).get
}
lazy val dummyService = new RepositoryService with AccountService with IssuesService with PullRequestService
with CommitStatusService (){}
def generateNewUserWithDBRepository(userName:String, repositoryName:String)(implicit s:Session):Account = {
val ac = generateNewAccount(userName)
dummyService.createRepository(repositoryName, userName, None, false)
ac
}
def generateNewIssue(userName:String, repositoryName:String, requestUserName:String="root")(implicit s:Session): Int = {
dummyService.createIssue(
owner = userName,
repository = repositoryName,
loginUser = requestUserName,
title = "issue title",
content = None,
assignedUserName = None,
milestoneId = None,
isPullRequest = true)
}
def generateNewPullRequest(base:String, request:String)(implicit s:Session):(Issue, PullRequest) = {
val Array(baseUserName, baseRepositoryName, baesBranch)=base.split("/")
val Array(requestUserName, requestRepositoryName, requestBranch)=request.split("/")
val issueId = generateNewIssue(baseUserName, baseRepositoryName, requestUserName)
dummyService.createPullRequest(
originUserName = baseUserName,
originRepositoryName = baseRepositoryName,
issueId = issueId,
originBranch = baesBranch,
requestUserName = requestUserName,
requestRepositoryName = requestRepositoryName,
requestBranch = requestBranch,
commitIdFrom = baesBranch,
commitIdTo = requestBranch)
dummyService.getPullRequest(baseUserName, baseRepositoryName, issueId).get
}
} }

View File

@@ -0,0 +1,42 @@
package gitbucket.core.service
import org.specs2.mutable.Specification
class WebHookServiceSpec extends Specification with ServiceSpecBase {
lazy val service = new WebHookPullRequestService with AccountService with RepositoryService with PullRequestService with IssuesService
"WebHookPullRequestService.getPullRequestsByRequestForWebhook" should {
"find from request branch" in { withTestDB { implicit session =>
val user1 = generateNewUserWithDBRepository("user1","repo1")
val user2 = generateNewUserWithDBRepository("user2","repo2")
val user3 = generateNewUserWithDBRepository("user3","repo3")
val (issue1, pullreq1) = generateNewPullRequest("user1/repo1/master1", "user2/repo2/master2")
val (issue3, pullreq3) = generateNewPullRequest("user3/repo3/master3", "user2/repo2/master2")
val (issue32, pullreq32) = generateNewPullRequest("user3/repo3/master32", "user2/repo2/master2")
generateNewPullRequest("user2/repo2/master2", "user1/repo1/master2")
service.addWebHookURL("user1", "repo1", "webhook1-1")
service.addWebHookURL("user1", "repo1", "webhook1-2")
service.addWebHookURL("user2", "repo2", "webhook2-1")
service.addWebHookURL("user2", "repo2", "webhook2-2")
service.addWebHookURL("user3", "repo3", "webhook3-1")
service.addWebHookURL("user3", "repo3", "webhook3-2")
service.getPullRequestsByRequestForWebhook("user1","repo1","master1") must_== Map.empty
var r = service.getPullRequestsByRequestForWebhook("user2","repo2","master2").mapValues(_.map(_.url).toSet)
r.size must_== 3
r((issue1, pullreq1, user1, user2)) must_== Set("webhook1-1","webhook1-2")
r((issue3, pullreq3, user3, user2)) must_== Set("webhook3-1","webhook3-2")
r((issue32, pullreq32, user3, user2)) must_== Set("webhook3-1","webhook3-2")
// when closed, it not founds.
service.updateClosed("user1","repo1",issue1.issueId, true)
var r2 = service.getPullRequestsByRequestForWebhook("user2","repo2","master2").mapValues(_.map(_.url).toSet)
r2.size must_== 2
r2((issue3, pullreq3, user3, user2)) must_== Set("webhook3-1","webhook3-2")
r2((issue32, pullreq32, user3, user2)) must_== Set("webhook3-1","webhook3-2")
} }
}
}

View File

@@ -0,0 +1,15 @@
package gitbucket.core.util
import org.specs2.mutable._
class DirectorySpec extends Specification {
"GitBucketHome" should {
"set under target in test scope" in {
Directory.GitBucketHome mustEqual new java.io.File("target/gitbucket_home_for_test").getAbsolutePath
}
"exists" in {
new java.io.File(Directory.GitBucketHome).exists
}
}
}