mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-05 04:56:02 +01:00
Merge remote-tracking branch 'origin/api-support'
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/main/resources/update/3_1.sql
Normal file
42
src/main/resources/update/3_1.sql
Normal 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;
|
||||||
@@ -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, "/*")
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
26
src/main/scala/gitbucket/core/api/ApiComment.scala
Normal file
26
src/main/scala/gitbucket/core/api/ApiComment.scala
Normal 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)
|
||||||
|
}
|
||||||
48
src/main/scala/gitbucket/core/api/ApiCommit.scala
Normal file
48
src/main/scala/gitbucket/core/api/ApiCommit.scala
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/main/scala/gitbucket/core/api/ApiCommitListItem.scala
Normal file
42
src/main/scala/gitbucket/core/api/ApiCommitListItem.scala
Normal 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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/main/scala/gitbucket/core/api/ApiCommitStatus.scala
Normal file
38
src/main/scala/gitbucket/core/api/ApiCommitStatus.scala
Normal 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))
|
||||||
|
}
|
||||||
5
src/main/scala/gitbucket/core/api/ApiError.scala
Normal file
5
src/main/scala/gitbucket/core/api/ApiError.scala
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package gitbucket.core.api
|
||||||
|
|
||||||
|
case class ApiError(
|
||||||
|
message: String,
|
||||||
|
documentation_url: Option[String] = None)
|
||||||
31
src/main/scala/gitbucket/core/api/ApiIssue.scala
Normal file
31
src/main/scala/gitbucket/core/api/ApiIssue.scala
Normal 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)
|
||||||
|
}
|
||||||
6
src/main/scala/gitbucket/core/api/ApiPath.scala
Normal file
6
src/main/scala/gitbucket/core/api/ApiPath.scala
Normal 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)
|
||||||
25
src/main/scala/gitbucket/core/api/ApiPersonIdent.scala
Normal file
25
src/main/scala/gitbucket/core/api/ApiPersonIdent.scala
Normal 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)
|
||||||
|
}
|
||||||
59
src/main/scala/gitbucket/core/api/ApiPullRequest.scala
Normal file
59
src/main/scala/gitbucket/core/api/ApiPullRequest.scala
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/main/scala/gitbucket/core/api/ApiRepository.scala
Normal file
48
src/main/scala/gitbucket/core/api/ApiRepository.scala
Normal 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))
|
||||||
|
|
||||||
|
}
|
||||||
36
src/main/scala/gitbucket/core/api/ApiUser.scala
Normal file
36
src/main/scala/gitbucket/core/api/ApiUser.scala
Normal 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
|
||||||
|
)
|
||||||
|
}
|
||||||
7
src/main/scala/gitbucket/core/api/CreateAComment.scala
Normal file
7
src/main/scala/gitbucket/core/api/CreateAComment.scala
Normal 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)
|
||||||
26
src/main/scala/gitbucket/core/api/CreateAStatus.scala
Normal file
26
src/main/scala/gitbucket/core/api/CreateAStatus.scala
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/main/scala/gitbucket/core/api/JsonFormat.scala
Normal file
44
src/main/scala/gitbucket/core/api/JsonFormat.scala
Normal 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))
|
||||||
|
}
|
||||||
@@ -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){
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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."))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 _ =>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/main/scala/gitbucket/core/model/AccessToken.scala
Normal file
21
src/main/scala/gitbucket/core/model/AccessToken.scala
Normal 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
|
||||||
|
)
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
83
src/main/scala/gitbucket/core/model/CommitStatus.scala
Normal file
83
src/main/scala/gitbucket/core/model/CommitStatus.scala
Normal 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(_)))
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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])
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
172
src/main/scala/gitbucket/core/service/MergeService.scala
Normal file
172
src/main/scala/gitbucket/core/service/MergeService.scala
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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._
|
||||||
|
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 _ =>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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(_))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
18
src/main/scala/gitbucket/core/util/RepoitoryName.scala
Normal file
18
src/main/scala/gitbucket/core/util/RepoitoryName.scala
Normal 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)
|
||||||
|
}
|
||||||
@@ -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 => "✔"
|
||||||
|
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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
57
src/main/twirl/gitbucket/core/account/application.scala.html
Normal file
57
src/main/twirl/gitbucket/core/account/application.scala.html
Normal 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>
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/main/twirl/gitbucket/core/issues/commitstatus.scala.html
Normal file
19
src/main/twirl/gitbucket/core/issues/commitstatus.scala.html
Normal 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){
|
||||||
|
✔
|
||||||
|
}else{
|
||||||
|
×
|
||||||
|
}</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 &&
|
||||||
|
|||||||
@@ -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 can’t automatically merge this pull request.</span>
|
<span class="strong">We can’t 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();
|
||||||
|
|||||||
@@ -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 */
|
||||||
/****************************************************************************/
|
/****************************************************************************/
|
||||||
|
|||||||
281
src/test/scala/gitbucket/core/api/JsonFormatSpec.scala
Normal file
281
src/test/scala/gitbucket/core/api/JsonFormatSpec.scala
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/test/scala/gitbucket/core/model/CommitStateSpec.scala
Normal file
26
src/test/scala/gitbucket/core/model/CommitStateSpec.scala
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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))
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
} }
|
||||||
|
}
|
||||||
|
}
|
||||||
159
src/test/scala/gitbucket/core/service/MergeServiceSpec.scala
Normal file
159
src/test/scala/gitbucket/core/service/MergeServiceSpec.scala
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
} }
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/test/scala/gitbucket/core/util/DirectorySpec.scala
Normal file
15
src/test/scala/gitbucket/core/util/DirectorySpec.scala
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user