diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala index b266261..30c507a 100644 --- a/src/main/scala/ScalatraBootstrap.scala +++ b/src/main/scala/ScalatraBootstrap.scala @@ -27,9 +27,10 @@ } context.mount(new IndexController, "/") + context.mount(new ApiController, "/api/v3") context.mount(new FileUploadController, "/upload") + context.mount(new SystemSettingsController, "/admin") context.mount(new DashboardController, "/*") - context.mount(new SystemSettingsController, "/*") context.mount(new AccountController, "/*") context.mount(new RepositoryViewerController, "/*") context.mount(new WikiController, "/*") diff --git a/src/main/scala/gitbucket/core/controller/AccountController.scala b/src/main/scala/gitbucket/core/controller/AccountController.scala index 54bdc58..5ddc010 100644 --- a/src/main/scala/gitbucket/core/controller/AccountController.scala +++ b/src/main/scala/gitbucket/core/controller/AccountController.scala @@ -1,7 +1,6 @@ package gitbucket.core.controller import gitbucket.core.account.html -import gitbucket.core.api._ import gitbucket.core.helper import gitbucket.core.model.GroupMember import gitbucket.core.service._ @@ -14,22 +13,19 @@ import io.github.gitbucket.scalatra.forms._ import org.apache.commons.io.FileUtils -import org.eclipse.jgit.api.Git -import org.eclipse.jgit.dircache.DirCache -import org.eclipse.jgit.lib.{FileMode, Constants} import org.scalatra.i18n.Messages class AccountController extends AccountControllerBase with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator - with AccessTokenService with WebHookService + with AccessTokenService with WebHookService with RepositoryCreationService trait AccountControllerBase extends AccountManagementControllerBase { self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator - with AccessTokenService with WebHookService => + with AccessTokenService with WebHookService with RepositoryCreationService => case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String, url: Option[String], fileId: Option[String]) @@ -156,25 +152,6 @@ } } - /** - * 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 { val userName = params("userName") getAccountByUserName(userName).map { x => @@ -367,7 +344,7 @@ post("/new", newRepositoryForm)(usersOnly { form => LockUtil.lock(s"${form.owner}/${form.name}"){ if(getRepository(form.owner, form.name).isEmpty){ - createRepository(form.owner, form.name, form.description, form.isPrivate, form.createReadme) + createRepository(context.loginAccount.get, form.owner, form.name, form.description, form.isPrivate, form.createReadme) } // redirect to the repository @@ -375,54 +352,6 @@ } }) - /** - * Create user repository - * https://developer.github.com/v3/repos/#create - */ - post("/api/v3/user/repos")(usersOnly { - val owner = context.loginAccount.get.userName - (for { - data <- extractFromJsonBody[CreateARepository] if data.isValid - } yield { - LockUtil.lock(s"${owner}/${data.name}") { - if(getRepository(owner, data.name).isEmpty){ - createRepository(owner, data.name, data.description, data.`private`, data.auto_init) - val repository = getRepository(owner, data.name).get - JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get))) - } else { - ApiError( - "A repository with this name already exists on this account", - Some("https://developer.github.com/v3/repos/#create") - ) - } - } - }) getOrElse NotFound - }) - - /** - * Create group repository - * https://developer.github.com/v3/repos/#create - */ - post("/api/v3/orgs/:org/repos")(managersOnly { - val groupName = params("org") - (for { - data <- extractFromJsonBody[CreateARepository] if data.isValid - } yield { - LockUtil.lock(s"${groupName}/${data.name}") { - if(getRepository(groupName, data.name).isEmpty){ - createRepository(groupName, data.name, data.description, data.`private`, data.auto_init) - val repository = getRepository(groupName, data.name).get - JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get))) - } else { - ApiError( - "A repository with this name already exists for this group", - Some("https://developer.github.com/v3/repos/#create") - ) - } - } - }) getOrElse NotFound - }) - get("/:owner/:repository/fork")(readableUsersOnly { repository => val loginAccount = context.loginAccount.get val loginUserName = loginAccount.userName @@ -456,7 +385,7 @@ val originUserName = repository.repository.originUserName.getOrElse(repository.owner) val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name) - createRepository( + insertRepository( repositoryName = repository.name, userName = accountName, description = repository.repository.description, @@ -496,68 +425,6 @@ } }) - private def createRepository(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) { - val ownerAccount = getAccountByUserName(owner).get - val loginAccount = context.loginAccount.get - val loginUserName = loginAccount.userName - - // Insert to the database at first - createRepository(name, owner, description, isPrivate) - - // Add collaborators for group repository - if(ownerAccount.isGroupAccount){ - getGroupMembers(owner).foreach { member => - addCollaborator(owner, name, member.userName) - } - } - - // Insert default labels - insertDefaultLabels(owner, name) - - // Create the actual repository - val gitdir = getRepositoryDir(owner, name) - JGitUtil.initRepository(gitdir) - - if(createReadme){ - using(Git.open(gitdir)){ git => - val builder = DirCache.newInCore.builder() - val inserter = git.getRepository.newObjectInserter() - val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") - val content = if(description.nonEmpty){ - name + "\n" + - "===============\n" + - "\n" + - description.get - } else { - name + "\n" + - "===============\n" - } - - builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE, - inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")))) - builder.finish() - - JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), - Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit") - } - } - - // Create Wiki repository - createWikiRepository(loginAccount, owner, name) - - // Record activity - recordCreateRepositoryActivity(owner, name, loginUserName) - } - - private def insertDefaultLabels(userName: String, repositoryName: String): Unit = { - createLabel(userName, repositoryName, "bug", "fc2929") - createLabel(userName, repositoryName, "duplicate", "cccccc") - createLabel(userName, repositoryName, "enhancement", "84b6eb") - createLabel(userName, repositoryName, "invalid", "e6e6e6") - createLabel(userName, repositoryName, "question", "cc317c") - createLabel(userName, repositoryName, "wontfix", "ffffff") - } - private def existsAccount: Constraint = new Constraint(){ override def validate(name: String, value: String, messages: Messages): Option[String] = if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None diff --git a/src/main/scala/gitbucket/core/controller/ApiController.scala b/src/main/scala/gitbucket/core/controller/ApiController.scala new file mode 100644 index 0000000..11d764b --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/ApiController.scala @@ -0,0 +1,389 @@ +package gitbucket.core.controller + +import gitbucket.core.api._ +import gitbucket.core.model._ +import gitbucket.core.service.IssuesService.IssueSearchCondition +import gitbucket.core.service.PullRequestService._ +import gitbucket.core.service._ +import gitbucket.core.util.ControlUtil._ +import gitbucket.core.util.Directory._ +import gitbucket.core.util.JGitUtil.CommitInfo +import gitbucket.core.util._ +import gitbucket.core.util.Implicits._ +import org.eclipse.jgit.api.Git +import org.scalatra.{NoContent, UnprocessableEntity, Created} +import scala.collection.JavaConverters._ + +class ApiController extends ApiControllerBase + with RepositoryService + with AccountService + with ProtectedBranchService + with IssuesService + with LabelsService + with PullRequestService + with CommitStatusService + with RepositoryCreationService + with HandleCommentService + with WebHookService + with WebHookPullRequestService + with WebHookIssueCommentService + with WikiService + with ActivityService + with OwnerAuthenticator + with UsersAuthenticator + with GroupManagerAuthenticator + with ReferrerAuthenticator + with ReadableUsersAuthenticator + with CollaboratorsAuthenticator + +trait ApiControllerBase extends ControllerBase { + self: RepositoryService + with AccountService + with ProtectedBranchService + with IssuesService + with LabelsService + with PullRequestService + with CommitStatusService + with RepositoryCreationService + with HandleCommentService + with OwnerAuthenticator + with UsersAuthenticator + with GroupManagerAuthenticator + with ReferrerAuthenticator + with ReadableUsersAuthenticator + with CollaboratorsAuthenticator => + + /** + * 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 + } + + /** + * Create user repository + * https://developer.github.com/v3/repos/#create + */ + post("/api/v3/user/repos")(usersOnly { + val owner = context.loginAccount.get.userName + (for { + data <- extractFromJsonBody[CreateARepository] if data.isValid + } yield { + LockUtil.lock(s"${owner}/${data.name}") { + if(getRepository(owner, data.name).isEmpty){ + createRepository(context.loginAccount.get, owner, data.name, data.description, data.`private`, data.auto_init) + val repository = getRepository(owner, data.name).get + JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get))) + } else { + ApiError( + "A repository with this name already exists on this account", + Some("https://developer.github.com/v3/repos/#create") + ) + } + } + }) getOrElse NotFound + }) + + /** + * Create group repository + * https://developer.github.com/v3/repos/#create + */ + post("/api/v3/orgs/:org/repos")(managersOnly { + val groupName = params("org") + (for { + data <- extractFromJsonBody[CreateARepository] if data.isValid + } yield { + LockUtil.lock(s"${groupName}/${data.name}") { + if(getRepository(groupName, data.name).isEmpty){ + createRepository(context.loginAccount.get, groupName, data.name, data.description, data.`private`, data.auto_init) + val repository = getRepository(groupName, data.name).get + JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get))) + } else { + ApiError( + "A repository with this name already exists for this group", + Some("https://developer.github.com/v3/repos/#create") + ) + } + } + }) getOrElse NotFound + }) + + /** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */ + patch("/api/v3/repos/:owner/:repo/branches/:branch")(ownerOnly { repository => + import gitbucket.core.api._ + (for{ + branch <- params.get("branch") if repository.branchList.find(_ == branch).isDefined + protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection) + } yield { + if(protection.enabled){ + enableBranchProtection(repository.owner, repository.name, branch, protection.status.enforcement_level == ApiBranchProtection.Everyone, protection.status.contexts) + } else { + disableBranchProtection(repository.owner, repository.name, branch) + } + JsonFormat(ApiBranch(branch, protection)(RepositoryName(repository))) + }) getOrElse NotFound + }) + + /** + * @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.")) + } + + /** + * 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, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) }) + }).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 + issue <- getIssue(repository.owner, repository.name, issueId.toString) + body <- extractFromJsonBody[CreateAComment].map(_.body) if ! body.isEmpty + action = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName)) + (issue, id) <- handleComment(issue, Some(body), repository, action) + issueComment <- getComment(repository.owner, repository.name, id.toString()) + } yield { + JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get), issue.isPullRequest)) + }) getOrElse NotFound + }) + + /** + * List all labels for this repository + * https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository + */ + get("/api/v3/repos/:owner/:repository/labels")(referrersOnly { repository => + JsonFormat(getLabels(repository.owner, repository.name).map { label => + ApiLabel(label, RepositoryName(repository)) + }) + }) + + /** + * Get a single label + * https://developer.github.com/v3/issues/labels/#get-a-single-label + */ + get("/api/v3/repos/:owner/:repository/labels/:labelName")(referrersOnly { repository => + getLabel(repository.owner, repository.name, params("labelName")).map { label => + JsonFormat(ApiLabel(label, RepositoryName(repository))) + } getOrElse NotFound() + }) + + /** + * Create a label + * https://developer.github.com/v3/issues/labels/#create-a-label + */ + post("/api/v3/repos/:owner/:repository/labels")(collaboratorsOnly { repository => + (for{ + data <- extractFromJsonBody[CreateALabel] if data.isValid + } yield { + LockUtil.lock(RepositoryName(repository).fullName) { + if (getLabel(repository.owner, repository.name, data.name).isEmpty) { + val labelId = createLabel(repository.owner, repository.name, data.name, data.color) + getLabel(repository.owner, repository.name, labelId).map { label => + Created(JsonFormat(ApiLabel(label, RepositoryName(repository)))) + } getOrElse NotFound() + } else { + // TODO ApiError should support errors field to enhance compatibility of GitHub API + UnprocessableEntity(ApiError( + "Validation Failed", + Some("https://developer.github.com/v3/issues/labels/#create-a-label") + )) + } + } + }) getOrElse NotFound() + }) + + /** + * Update a label + * https://developer.github.com/v3/issues/labels/#update-a-label + */ + patch("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository => + (for{ + data <- extractFromJsonBody[CreateALabel] if data.isValid + } yield { + LockUtil.lock(RepositoryName(repository).fullName) { + getLabel(repository.owner, repository.name, params("labelName")).map { label => + if (getLabel(repository.owner, repository.name, data.name).isEmpty) { + updateLabel(repository.owner, repository.name, label.labelId, data.name, data.color) + JsonFormat(ApiLabel( + getLabel(repository.owner, repository.name, label.labelId).get, + RepositoryName(repository))) + } else { + // TODO ApiError should support errors field to enhance compatibility of GitHub API + UnprocessableEntity(ApiError( + "Validation Failed", + Some("https://developer.github.com/v3/issues/labels/#create-a-label"))) + } + } getOrElse NotFound() + } + }) getOrElse NotFound() + }) + + /** + * Delete a label + * https://developer.github.com/v3/issues/labels/#delete-a-label + */ + delete("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository => + LockUtil.lock(RepositoryName(repository).fullName) { + getLabel(repository.owner, repository.name, params("labelName")).map { label => + deleteLabel(repository.owner, repository.name, label.labelId) + NoContent() + } getOrElse NotFound() + } + }) + + /** + * 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)) }) + }) + + /** + * 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.openedUserName), Set()) + baseOwner <- users.get(repository.owner) + headOwner <- users.get(pullRequest.requestUserName) + issueUser <- users.get(issue.openedUserName) + headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName) + } 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 + }) + + /** + * https://developer.github.com/v3/repos/#get + */ + get("/api/v3/repos/:owner/:repository")(referrersOnly { repository => + JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(repository.owner).get))) + }) + + /** + * 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. + */ + val listStatusesRoute = 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/#list-statuses-for-a-specific-ref + * + * legacy route + */ + get("/api/v3/repos/:owner/:repo/statuses/:ref"){ + listStatusesRoute.action() + } + + /** + * 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 + }) + + private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean = + hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName + +} + diff --git a/src/main/scala/gitbucket/core/controller/IndexController.scala b/src/main/scala/gitbucket/core/controller/IndexController.scala index 5c83cbb..8f0ed76 100644 --- a/src/main/scala/gitbucket/core/controller/IndexController.scala +++ b/src/main/scala/gitbucket/core/controller/IndexController.scala @@ -121,16 +121,6 @@ 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.")) - } - // TODO Move to RepositoryViwerController? post("/search", searchForm){ form => redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}") diff --git a/src/main/scala/gitbucket/core/controller/IssuesController.scala b/src/main/scala/gitbucket/core/controller/IssuesController.scala index 6ef4964..912c4f9 100644 --- a/src/main/scala/gitbucket/core/controller/IssuesController.scala +++ b/src/main/scala/gitbucket/core/controller/IssuesController.scala @@ -1,8 +1,6 @@ package gitbucket.core.controller -import gitbucket.core.api._ import gitbucket.core.issues.html -import gitbucket.core.model.Issue import gitbucket.core.service.IssuesService._ import gitbucket.core.service._ import gitbucket.core.util.ControlUtil._ @@ -16,11 +14,11 @@ 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 HandleCommentService with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService 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 HandleCommentService with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService => case class IssueCreateForm(title: String, content: Option[String], @@ -78,18 +76,6 @@ } }) - /** - * 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, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) }) - }).getOrElse(NotFound) - }) - get("/:owner/:repository/issues/new")(readableUsersOnly { repository => defining(repository.owner, repository.name){ case (owner, name) => html.create( @@ -128,7 +114,7 @@ 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(""), context.loginAccount.get) // call web hooks callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get) @@ -150,7 +136,7 @@ // update issue updateIssue(owner, name, issue.issueId, title, issue.content) // extract references and create refer comment - createReferComment(owner, name, issue.copy(title = title), title) + createReferComment(owner, name, issue.copy(title = title), title, context.loginAccount.get) redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") } else Unauthorized @@ -165,7 +151,7 @@ // update issue updateIssue(owner, name, issue.issueId, issue.title, content) // extract references and create refer comment - createReferComment(owner, name, issue, content.getOrElse("")) + createReferComment(owner, name, issue, content.getOrElse(""), context.loginAccount.get) redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") } else Unauthorized @@ -174,30 +160,22 @@ }) post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => - handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) => - redirect(s"/${repository.owner}/${repository.name}/${ - if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") + getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue => + val actionOpt = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName)) + handleComment(issue, Some(form.content), repository, actionOpt) map { case (issue, id) => + redirect(s"/${repository.owner}/${repository.name}/${ + if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") + } } 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, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get), issue.isPullRequest)) - }) getOrElse NotFound - }) - post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) => - handleComment(form.issueId, form.content, repository)() map { case (issue, id) => - redirect(s"/${repository.owner}/${repository.name}/${ - if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") + getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue => + val actionOpt = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName)) + handleComment(issue, form.content, repository, actionOpt) map { case (issue, id) => + redirect(s"/${repository.owner}/${repository.name}/${ + if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") + } } getOrElse NotFound }) @@ -315,8 +293,16 @@ post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => defining(params.get("value")){ action => action match { - case Some("open") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("reopen")) } - case Some("close") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("close")) } + case Some("open") => executeBatch(repository) { issueId => + getIssue(repository.owner, repository.name, issueId.toString).foreach { issue => + handleComment(issue, None, repository, Some("reopen")) + } + } + case Some("close") => executeBatch(repository) { issueId => + getIssue(repository.owner, repository.name, issueId.toString).foreach { issue => + handleComment(issue, None, repository, Some("close")) + } + } case _ => // TODO BadRequest } } @@ -373,99 +359,6 @@ } } - // TODO Same method exists in PullRequestController. Should it moved to IssueService? - private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = { - StringUtil.extractIssueId(message).foreach { issueId => - val content = fromIssue.issueId + ":" + fromIssue.title - if(getIssue(owner, repository, issueId).isDefined){ - // Not add if refer comment already exist. - if(!getComments(owner, repository, issueId.toInt).exists { x => x.action == "refer" && x.content == content }) { - createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, content, "refer") - } - } - } - } - - /** - * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]] - */ - private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo) - (getAction: Issue => Option[String] = - p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = { - - defining(repository.owner, repository.name){ case (owner, name) => - val userName = context.loginAccount.get.userName - - getIssue(owner, name, issueId.toString) flatMap { issue => - val (action, recordActivity) = - getAction(issue) - .collect { - case "close" if(!issue.closed) => true -> - (Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _)) - case "reopen" if(issue.closed) => false -> - (Some("reopen") -> Some(recordReopenIssueActivity _)) - } - .map { case (closed, t) => - updateClosed(owner, name, issueId, closed) - t - } - .getOrElse(None -> None) - - val commentId = (content, action) match { - case (None, None) => None - case (None, Some(action)) => Some(createComment(owner, name, userName, issueId, action.capitalize, action)) - case (Some(content), _) => Some(createComment(owner, name, userName, issueId, content, action.map(_+ "_comment").getOrElse("comment"))) - } - - // record comment activity if comment is entered - content foreach { - (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _) - (owner, name, userName, issueId, _) - } - recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) ) - - // extract references and create refer comment - content.map { 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 - Notifier() match { - case f => - content foreach { - f.toNotify(repository, issue, _){ - Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${ - if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId.get}") - } - } - action foreach { - f.toNotify(repository, issue, _){ - Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") - } - } - } - - commentId.map( issue -> _ ) - } - } - } - private def searchIssues(repository: RepositoryService.RepositoryInfo) = { defining(repository.owner, repository.name){ case (owner, repoName) => val page = IssueSearchCondition.page(request) diff --git a/src/main/scala/gitbucket/core/controller/LabelsController.scala b/src/main/scala/gitbucket/core/controller/LabelsController.scala index 4074c61..9ebdf6e 100644 --- a/src/main/scala/gitbucket/core/controller/LabelsController.scala +++ b/src/main/scala/gitbucket/core/controller/LabelsController.scala @@ -1,13 +1,12 @@ package gitbucket.core.controller -import gitbucket.core.api.{ApiError, CreateALabel, ApiLabel, JsonFormat} import gitbucket.core.issues.labels.html import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService} -import gitbucket.core.util.{LockUtil, RepositoryName, ReferrerAuthenticator, CollaboratorsAuthenticator} +import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator} import gitbucket.core.util.Implicits._ import io.github.gitbucket.scalatra.forms._ import org.scalatra.i18n.Messages -import org.scalatra.{NoContent, UnprocessableEntity, Created, Ok} +import org.scalatra.Ok class LabelsController extends LabelsControllerBase with LabelsService with IssuesService with RepositoryService with AccountService @@ -32,26 +31,6 @@ hasWritePermission(repository.owner, repository.name, context.loginAccount)) }) - /** - * List all labels for this repository - * https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository - */ - get("/api/v3/repos/:owner/:repository/labels")(referrersOnly { repository => - JsonFormat(getLabels(repository.owner, repository.name).map { label => - ApiLabel(label, RepositoryName(repository)) - }) - }) - - /** - * Get a single label - * https://developer.github.com/v3/issues/labels/#get-a-single-label - */ - get("/api/v3/repos/:owner/:repository/labels/:labelName")(referrersOnly { repository => - getLabel(repository.owner, repository.name, params("labelName")).map { label => - JsonFormat(ApiLabel(label, RepositoryName(repository))) - } getOrElse NotFound() - }) - ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository => html.edit(None, repository) }) @@ -66,31 +45,6 @@ hasWritePermission(repository.owner, repository.name, context.loginAccount)) }) - /** - * Create a label - * https://developer.github.com/v3/issues/labels/#create-a-label - */ - post("/api/v3/repos/:owner/:repository/labels")(collaboratorsOnly { repository => - (for{ - data <- extractFromJsonBody[CreateALabel] if data.isValid - } yield { - LockUtil.lock(RepositoryName(repository).fullName) { - if (getLabel(repository.owner, repository.name, data.name).isEmpty) { - val labelId = createLabel(repository.owner, repository.name, data.name, data.color) - getLabel(repository.owner, repository.name, labelId).map { label => - Created(JsonFormat(ApiLabel(label, RepositoryName(repository)))) - } getOrElse NotFound() - } else { - // TODO ApiError should support errors field to enhance compatibility of GitHub API - UnprocessableEntity(ApiError( - "Validation Failed", - Some("https://developer.github.com/v3/issues/labels/#create-a-label") - )) - } - } - }) getOrElse NotFound() - }) - ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository => getLabel(repository.owner, repository.name, params("labelId").toInt).map { label => html.edit(Some(label), repository) @@ -107,51 +61,12 @@ hasWritePermission(repository.owner, repository.name, context.loginAccount)) }) - /** - * Update a label - * https://developer.github.com/v3/issues/labels/#update-a-label - */ - patch("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository => - (for{ - data <- extractFromJsonBody[CreateALabel] if data.isValid - } yield { - LockUtil.lock(RepositoryName(repository).fullName) { - getLabel(repository.owner, repository.name, params("labelName")).map { label => - if (getLabel(repository.owner, repository.name, data.name).isEmpty) { - updateLabel(repository.owner, repository.name, label.labelId, data.name, data.color) - JsonFormat(ApiLabel( - getLabel(repository.owner, repository.name, label.labelId).get, - RepositoryName(repository))) - } else { - // TODO ApiError should support errors field to enhance compatibility of GitHub API - UnprocessableEntity(ApiError( - "Validation Failed", - Some("https://developer.github.com/v3/issues/labels/#create-a-label"))) - } - } getOrElse NotFound() - } - }) getOrElse NotFound() - }) - ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository => deleteLabel(repository.owner, repository.name, params("labelId").toInt) Ok() }) /** - * Delete a label - * https://developer.github.com/v3/issues/labels/#delete-a-label - */ - delete("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository => - LockUtil.lock(RepositoryName(repository).fullName) { - getLabel(repository.owner, repository.name, params("labelName")).map { label => - deleteLabel(repository.owner, repository.name, label.labelId) - NoContent() - } getOrElse NotFound() - } - }) - - /** * Constraint for the identifier such as user name, repository name or page name. */ private def labelName: Constraint = new Constraint(){ diff --git a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala index 11141ff..763fcdb 100644 --- a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala +++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala @@ -1,7 +1,6 @@ package gitbucket.core.controller -import gitbucket.core.api._ -import gitbucket.core.model.{Account, CommitStatus, CommitState, Repository, PullRequest, Issue, WebHook} +import gitbucket.core.model.WebHook import gitbucket.core.pulls.html import gitbucket.core.service.CommitStatusService import gitbucket.core.service.MergeService @@ -82,24 +81,6 @@ } }) - /** - * 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 => params("id").toIntOpt.flatMap{ issueId => val owner = repository.owner @@ -126,47 +107,6 @@ } 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.openedUserName), Set()) - baseOwner <- users.get(repository.owner) - headOwner <- users.get(pullRequest.requestUserName) - issueUser <- users.get(issue.openedUserName) - headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName) - } 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")(referrersOnly { repository => params("id").toIntOpt.flatMap{ issueId => val owner = repository.owner @@ -523,7 +463,7 @@ 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(""), context.loginAccount.get) // notifications Notifier().toNotify(repository, issue, form.content.getOrElse("")){ @@ -535,19 +475,6 @@ } }) - // TODO Same method exists in IssueController. Should it moved to IssueService? - private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = { - StringUtil.extractIssueId(message).foreach { issueId => - val content = fromIssue.issueId + ":" + fromIssue.title - if(getIssue(owner, repository, issueId).isDefined){ - // Not add if refer comment already exist. - if(!getComments(owner, repository, issueId.toInt).exists { x => x.action == "refer" && x.content == content }) { - createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, content, "refer") - } - } - } - } - /** * Parses branch identifier and extracts owner and branch name as tuple. * @@ -611,14 +538,4 @@ hasWritePermission(owner, repoName, context.loginAccount)) } - // TODO: same as gitbucket.core.servlet.CommitLogHook ... - private def createIssueComment(owner: String, repository: String, commit: CommitInfo) = { - StringUtil.extractIssueId(commit.fullMessage).foreach { issueId => - if(getIssue(owner, repository, issueId).isDefined){ - getAccountByMailAddress(commit.committerEmailAddress).foreach { account => - createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit") - } - } - } - } } diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index 7867334..d15c424 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -142,22 +142,6 @@ } }) - /** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */ - patch("/api/v3/repos/:owner/:repo/branches/:branch")(ownerOnly { repository => - import gitbucket.core.api._ - (for{ - branch <- params.get("branch") if repository.branchList.find(_ == branch).isDefined - protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection) - } yield { - if(protection.enabled){ - enableBranchProtection(repository.owner, repository.name, branch, protection.status.enforcement_level == ApiBranchProtection.Everyone, protection.status.contexts) - } else { - disableBranchProtection(repository.owner, repository.name, branch) - } - JsonFormat(ApiBranch(branch, protection)(RepositoryName(repository))) - }) getOrElse NotFound - }) - /** * Display the Collaborators page. */ diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala index 1007cf9..e5680d3 100644 --- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala @@ -2,7 +2,6 @@ import javax.servlet.http.{HttpServletResponse, HttpServletRequest} -import gitbucket.core.api._ import gitbucket.core.plugin.PluginRegistry import gitbucket.core.repo.html import gitbucket.core.helper @@ -13,7 +12,7 @@ import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.Implicits._ import gitbucket.core.util.Directory._ -import gitbucket.core.model.{Account, CommitState, WebHook} +import gitbucket.core.model.{Account, WebHook} import gitbucket.core.service.WebHookService._ import gitbucket.core.view import gitbucket.core.view.helpers @@ -123,13 +122,6 @@ }) /** - * 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. */ get("/:owner/:repository/tree/*")(referrersOnly { repository => @@ -160,65 +152,6 @@ } }) - /** - * 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. - */ - val listStatusesRoute = 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/#list-statuses-for-a-specific-ref - * - * legacy route - */ - get("/api/v3/repos/:owner/:repo/statuses/:ref"){ - listStatusesRoute.action() - } - - /** - * 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 => val (branch, path) = splitPath(repository, multiParams("splat").head) val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName) diff --git a/src/main/scala/gitbucket/core/service/HandleCommentService.scala b/src/main/scala/gitbucket/core/service/HandleCommentService.scala new file mode 100644 index 0000000..6a1b068 --- /dev/null +++ b/src/main/scala/gitbucket/core/service/HandleCommentService.scala @@ -0,0 +1,91 @@ +package gitbucket.core.service + +import gitbucket.core.controller.Context +import gitbucket.core.model.Issue +import gitbucket.core.model.Profile._ +import gitbucket.core.util.ControlUtil._ +import gitbucket.core.util.Implicits._ +import gitbucket.core.util.Notifier +import profile.simple._ + +trait HandleCommentService { + self: RepositoryService with IssuesService with ActivityService + with WebHookService with WebHookIssueCommentService with WebHookPullRequestService => + + /** + * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]] + */ + def handleComment(issue: Issue, content: Option[String], repository: RepositoryService.RepositoryInfo, actionOpt: Option[String]) + (implicit context: Context, s: Session) = { + + defining(repository.owner, repository.name){ case (owner, name) => + val userName = context.loginAccount.get.userName + + val (action, recordActivity) = actionOpt + .collect { + case "close" if(!issue.closed) => true -> + (Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _)) + case "reopen" if(issue.closed) => false -> + (Some("reopen") -> Some(recordReopenIssueActivity _)) + } + .map { case (closed, t) => + updateClosed(owner, name, issue.issueId, closed) + t + } + .getOrElse(None -> None) + + val commentId = (content, action) match { + case (None, None) => None + case (None, Some(action)) => Some(createComment(owner, name, userName, issue.issueId, action.capitalize, action)) + case (Some(content), _) => Some(createComment(owner, name, userName, issue.issueId, content, action.map(_+ "_comment").getOrElse("comment"))) + } + + // record comment activity if comment is entered + content foreach { + (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _) + (owner, name, userName, issue.issueId, _) + } + recordActivity foreach ( _ (owner, name, userName, issue.issueId, issue.title) ) + + // extract references and create refer comment + content.map { content => + createReferComment(owner, name, issue, content, context.loginAccount.get) + } + + // 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 + Notifier() match { + case f => + content foreach { + f.toNotify(repository, issue, _){ + Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${ + if(issue.isPullRequest) "pull" else "issues"}/${issue.issueId}#comment-${commentId.get}") + } + } + action foreach { + f.toNotify(repository, issue, _){ + Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issue.issueId}") + } + } + } + + commentId.map( issue -> _ ) + } + } + +} diff --git a/src/main/scala/gitbucket/core/service/IssuesService.scala b/src/main/scala/gitbucket/core/service/IssuesService.scala index 82c8c58..86430a9 100644 --- a/src/main/scala/gitbucket/core/service/IssuesService.scala +++ b/src/main/scala/gitbucket/core/service/IssuesService.scala @@ -1,6 +1,8 @@ package gitbucket.core.service import gitbucket.core.model.Profile._ +import gitbucket.core.util.JGitUtil.CommitInfo +import gitbucket.core.util.StringUtil import profile.simple._ import gitbucket.core.util.StringUtil._ @@ -12,6 +14,7 @@ trait IssuesService { + self: AccountService => import IssuesService._ def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) = @@ -394,6 +397,29 @@ } } } + + def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String, loginAccount: Account)(implicit s: Session) = { + StringUtil.extractIssueId(message).foreach { issueId => + val content = fromIssue.issueId + ":" + fromIssue.title + if(getIssue(owner, repository, issueId).isDefined){ + // Not add if refer comment already exist. + if(!getComments(owner, repository, issueId.toInt).exists { x => x.action == "refer" && x.content == content }) { + createComment(owner, repository, loginAccount.userName, issueId.toInt, content, "refer") + } + } + } + } + + def createIssueComment(owner: String, repository: String, commit: CommitInfo)(implicit s: Session) = { + StringUtil.extractIssueId(commit.fullMessage).foreach { issueId => + if(getIssue(owner, repository, issueId).isDefined){ + getAccountByMailAddress(commit.committerEmailAddress).foreach { account => + createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit") + } + } + } + } + } object IssuesService { diff --git a/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala b/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala new file mode 100644 index 0000000..90e0afd --- /dev/null +++ b/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala @@ -0,0 +1,79 @@ +package gitbucket.core.service + +import gitbucket.core.model.Profile._ +import gitbucket.core.util.ControlUtil._ +import gitbucket.core.util.Directory._ +import gitbucket.core.util.JGitUtil +import gitbucket.core.model.Account +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.dircache.DirCache +import org.eclipse.jgit.lib.{FileMode, Constants} +import profile.simple._ + +trait RepositoryCreationService { + self: AccountService with RepositoryService with LabelsService with WikiService with ActivityService => + + def createRepository(loginAccount: Account, owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) + (implicit s: Session) { + val ownerAccount = getAccountByUserName(owner).get + val loginUserName = loginAccount.userName + + // Insert to the database at first + insertRepository(name, owner, description, isPrivate) + + // Add collaborators for group repository + if(ownerAccount.isGroupAccount){ + getGroupMembers(owner).foreach { member => + addCollaborator(owner, name, member.userName) + } + } + + // Insert default labels + insertDefaultLabels(owner, name) + + // Create the actual repository + val gitdir = getRepositoryDir(owner, name) + JGitUtil.initRepository(gitdir) + + if(createReadme){ + using(Git.open(gitdir)){ git => + val builder = DirCache.newInCore.builder() + val inserter = git.getRepository.newObjectInserter() + val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") + val content = if(description.nonEmpty){ + name + "\n" + + "===============\n" + + "\n" + + description.get + } else { + name + "\n" + + "===============\n" + } + + builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE, + inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")))) + builder.finish() + + JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), + Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit") + } + } + + // Create Wiki repository + createWikiRepository(loginAccount, owner, name) + + // Record activity + recordCreateRepositoryActivity(owner, name, loginUserName) + } + + def insertDefaultLabels(userName: String, repositoryName: String)(implicit s: Session): Unit = { + createLabel(userName, repositoryName, "bug", "fc2929") + createLabel(userName, repositoryName, "duplicate", "cccccc") + createLabel(userName, repositoryName, "enhancement", "84b6eb") + createLabel(userName, repositoryName, "invalid", "e6e6e6") + createLabel(userName, repositoryName, "question", "cc317c") + createLabel(userName, repositoryName, "wontfix", "ffffff") + } + + +} diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala index 408c9b6..efc2e78 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala @@ -19,7 +19,7 @@ * @param originRepositoryName specify for the forked repository. (default is None) * @param originUserName specify for the forked repository. (default is None) */ - def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean, + def insertRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean, originRepositoryName: Option[String] = None, originUserName: Option[String] = None, parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None) (implicit s: Session): Unit = { diff --git a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala index 0583f61..047e5ed 100644 --- a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala +++ b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala @@ -10,7 +10,6 @@ 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 org.eclipse.jgit.api.Git @@ -168,7 +167,7 @@ if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) { if (issueCount > 0) { pushedIds.add(commit.id) - createIssueComment(commit) + createIssueComment(owner, repository, commit) // close issues if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){ closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository) @@ -230,13 +229,4 @@ } } - private def createIssueComment(commit: CommitInfo) = { - StringUtil.extractIssueId(commit.fullMessage).foreach { issueId => - if(getIssue(owner, repository, issueId).isDefined){ - getAccountByMailAddress(commit.committerEmailAddress).foreach { account => - createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit") - } - } - } - } } diff --git a/src/test/scala/gitbucket/core/service/CommitStatusServiceSpec.scala b/src/test/scala/gitbucket/core/service/CommitStatusServiceSpec.scala index 64eeb19..8c3d36b 100644 --- a/src/test/scala/gitbucket/core/service/CommitStatusServiceSpec.scala +++ b/src/test/scala/gitbucket/core/service/CommitStatusServiceSpec.scala @@ -33,7 +33,7 @@ now = fixture1.registeredDate) test("createCommitState can insert and update") { withTestDB { implicit session => val tester = generateNewAccount(fixture1.creator) - createRepository(fixture1.repositoryName,fixture1.userName,None,false) + insertRepository(fixture1.repositoryName,fixture1.userName,None,false) val id = generateFixture1(tester:Account) assert(getCommitStatus(fixture1.userName, fixture1.repositoryName, id) == Some(fixture1.copy(commitStatusId=id))) // other one can update @@ -60,14 +60,14 @@ test("getCommitStatus can find by commitId and context") { withTestDB { implicit session => val tester = generateNewAccount(fixture1.creator) - createRepository(fixture1.repositoryName,fixture1.userName,None,false) + insertRepository(fixture1.repositoryName,fixture1.userName,None,false) val id = generateFixture1(tester:Account) assert(getCommitStatus(fixture1.userName, fixture1.repositoryName, fixture1.commitId, fixture1.context) == Some(fixture1.copy(commitStatusId=id))) }} test("getCommitStatus can find by commitStatusId") { withTestDB { implicit session => val tester = generateNewAccount(fixture1.creator) - createRepository(fixture1.repositoryName,fixture1.userName,None,false) + insertRepository(fixture1.repositoryName,fixture1.userName,None,false) val id = generateFixture1(tester:Account) assert(getCommitStatus(fixture1.userName, fixture1.repositoryName, id) == Some(fixture1.copy(commitStatusId=id))) }} diff --git a/src/test/scala/gitbucket/core/service/PullRequestServiceSpec.scala b/src/test/scala/gitbucket/core/service/PullRequestServiceSpec.scala index 2127dbf..ca55903 100644 --- a/src/test/scala/gitbucket/core/service/PullRequestServiceSpec.scala +++ b/src/test/scala/gitbucket/core/service/PullRequestServiceSpec.scala @@ -3,7 +3,7 @@ import gitbucket.core.model._ import org.scalatest.FunSpec -class PullRequestServiceSpec extends FunSpec with ServiceSpecBase with PullRequestService with IssuesService { +class PullRequestServiceSpec extends FunSpec with ServiceSpecBase with PullRequestService with IssuesService with AccountService { def swap(r: (Issue, PullRequest)) = (r._2 -> r._1) diff --git a/src/test/scala/gitbucket/core/service/RepositoryServiceSpec.scala b/src/test/scala/gitbucket/core/service/RepositoryServiceSpec.scala index 9a343bd..0b13b4b 100644 --- a/src/test/scala/gitbucket/core/service/RepositoryServiceSpec.scala +++ b/src/test/scala/gitbucket/core/service/RepositoryServiceSpec.scala @@ -6,7 +6,7 @@ class RepositoryServiceSpec extends FunSuite with ServiceSpecBase with RepositoryService with AccountService{ test("renameRepository can rename CommitState, ProtectedBranches") { withTestDB { implicit session => val tester = generateNewAccount("tester") - createRepository("repo", "root", None, false) + insertRepository("repo", "root", None, false) val service = new CommitStatusService with ProtectedBranchService {} val id = service.createCommitStatus( userName = "root", diff --git a/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala b/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala index a7b383d..06aaaf7 100644 --- a/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala +++ b/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala @@ -42,7 +42,7 @@ def generateNewUserWithDBRepository(userName:String, repositoryName:String)(implicit s:Session):Account = { val ac = AccountService.getAccountByUserName(userName).getOrElse(generateNewAccount(userName)) - dummyService.createRepository(repositoryName, userName, None, false) + dummyService.insertRepository(repositoryName, userName, None, false) ac }