diff --git a/src/main/scala/gitbucket/core/api/AddACollaborator.scala b/src/main/scala/gitbucket/core/api/AddACollaborator.scala new file mode 100644 index 0000000..05b1768 --- /dev/null +++ b/src/main/scala/gitbucket/core/api/AddACollaborator.scala @@ -0,0 +1,9 @@ +package gitbucket.core.api + +case class AddACollaborator(permission: String) { + val role: String = permission match { + case "admin" => "ADMIN" + case "push" => "DEVELOPER" + case "pull" => "GUEST" + } +} diff --git a/src/main/scala/gitbucket/core/api/ApiGroup.scala b/src/main/scala/gitbucket/core/api/ApiGroup.scala new file mode 100644 index 0000000..5af6d4f --- /dev/null +++ b/src/main/scala/gitbucket/core/api/ApiGroup.scala @@ -0,0 +1,20 @@ +package gitbucket.core.api + +import java.util.Date + +import gitbucket.core.model.Account + +case class ApiGroup(login: String, description: Option[String], created_at: Date) { + val id = 0 // dummy id + val url = ApiPath(s"/api/v3/orgs/${login}") + val html_url = ApiPath(s"/${login}") + val avatar_url = ApiPath(s"/${login}/_avatar") +} + +object ApiGroup { + def apply(group: Account): ApiGroup = ApiGroup( + login = group.userName, + description = group.description, + created_at = group.registeredDate + ) +} diff --git a/src/main/scala/gitbucket/core/api/CreateAPullRequest.scala b/src/main/scala/gitbucket/core/api/CreateAPullRequest.scala new file mode 100644 index 0000000..aa253b8 --- /dev/null +++ b/src/main/scala/gitbucket/core/api/CreateAPullRequest.scala @@ -0,0 +1,16 @@ +package gitbucket.core.api + +case class CreateAPullRequest( + title: String, + head: String, + base: String, + body: Option[String], + maintainer_can_modify: Option[Boolean] +) + +case class CreateAPullRequestAlt( + issue: Integer, + head: String, + base: String, + maintainer_can_modify: Option[Boolean] +) diff --git a/src/main/scala/gitbucket/core/api/CreateAUser.scala b/src/main/scala/gitbucket/core/api/CreateAUser.scala new file mode 100644 index 0000000..f2cd9f0 --- /dev/null +++ b/src/main/scala/gitbucket/core/api/CreateAUser.scala @@ -0,0 +1,11 @@ +package gitbucket.core.api + +case class CreateAUser( + login: String, + password: String, + email: String, + fullName: Option[String], + isAdmin: Option[Boolean], + description: Option[String], + url: Option[String] +) diff --git a/src/main/scala/gitbucket/core/api/JsonFormat.scala b/src/main/scala/gitbucket/core/api/JsonFormat.scala index 8579926..96fbd69 100644 --- a/src/main/scala/gitbucket/core/api/JsonFormat.scala +++ b/src/main/scala/gitbucket/core/api/JsonFormat.scala @@ -24,6 +24,7 @@ }, { case x: Date => JString(OffsetDateTime.ofInstant(x.toInstant, ZoneId.of("UTC")).format(parserISO)) } ) ) + FieldSerializer[ApiUser]() + + FieldSerializer[ApiGroup]() + FieldSerializer[ApiPullRequest]() + FieldSerializer[ApiRepository]() + FieldSerializer[ApiCommitListItem.Parent]() + diff --git a/src/main/scala/gitbucket/core/api/UpdateAUser.scala b/src/main/scala/gitbucket/core/api/UpdateAUser.scala new file mode 100644 index 0000000..1981c78 --- /dev/null +++ b/src/main/scala/gitbucket/core/api/UpdateAUser.scala @@ -0,0 +1,11 @@ +package gitbucket.core.api + +case class UpdateAUser( + name: Option[String], + email: Option[String], + blog: Option[String], + company: Option[String], + location: Option[String], + hireable: Option[Boolean], + bio: Option[String] +) diff --git a/src/main/scala/gitbucket/core/controller/ApiController.scala b/src/main/scala/gitbucket/core/controller/ApiController.scala index 5e78b20..1cf08af 100644 --- a/src/main/scala/gitbucket/core/controller/ApiController.scala +++ b/src/main/scala/gitbucket/core/controller/ApiController.scala @@ -33,6 +33,7 @@ with RepositoryCreationService with IssueCreationService with HandleCommentService + with MergeService with WebHookService with WebHookPullRequestService with WebHookIssueCommentService @@ -40,6 +41,7 @@ with WikiService with ActivityService with PrioritiesService + with AdminAuthenticator with OwnerAuthenticator with UsersAuthenticator with GroupManagerAuthenticator diff --git a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala index 3b5ab5c..18fd38d 100644 --- a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala +++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala @@ -15,7 +15,7 @@ import gitbucket.core.util._ import org.scalatra.forms._ import org.eclipse.jgit.api.Git -import org.eclipse.jgit.lib.PersonIdent +import org.eclipse.jgit.lib.{ObjectId, PersonIdent} import org.eclipse.jgit.revwalk.RevWalk import org.scalatra.BadRequest @@ -412,97 +412,68 @@ .map(_.repository.repositoryName) }; originRepository <- getRepository(originOwner, originRepositoryName)) yield { - using( - Git.open(getRepositoryDir(originRepository.owner, originRepository.name)), - Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name)) - ) { - case (oldGit, newGit) => - val (oldId, newId) = - if (originRepository.branchList.contains(originId)) { - val forkedId2 = - forkedRepository.tags.collectFirst { case x if x.name == forkedId => x.id }.getOrElse(forkedId) + val (oldId, newId) = + getPullRequestCommitFromTo(originRepository, forkedRepository, originId, forkedId) - val originId2 = JGitUtil.getForkedCommitId( - oldGit, - newGit, - originRepository.owner, - originRepository.name, - originId, - forkedRepository.owner, - forkedRepository.name, - forkedId2 - ) + (oldId, newId) match { + case (Some(oldId), Some(newId)) => { + val (commits, diffs) = getRequestCompareInfo( + originRepository.owner, + originRepository.name, + oldId.getName, + forkedRepository.owner, + forkedRepository.name, + newId.getName + ) - (Option(oldGit.getRepository.resolve(originId2)), Option(newGit.getRepository.resolve(forkedId2))) - - } else { - val originId2 = - originRepository.tags.collectFirst { case x if x.name == originId => x.id }.getOrElse(originId) - val forkedId2 = - forkedRepository.tags.collectFirst { case x if x.name == forkedId => x.id }.getOrElse(forkedId) - - (Option(oldGit.getRepository.resolve(originId2)), Option(newGit.getRepository.resolve(forkedId2))) - } - - (oldId, newId) match { - case (Some(oldId), Some(newId)) => { - val (commits, diffs) = getRequestCompareInfo( - originRepository.owner, - originRepository.name, - oldId.getName, - forkedRepository.owner, - forkedRepository.name, - newId.getName - ) - - val title = if (commits.flatten.length == 1) { - commits.flatten.head.shortMessage - } else { - val text = forkedId.replaceAll("[\\-_]", " ") - text.substring(0, 1).toUpperCase + text.substring(1) - } - - html.compare( - title, - commits, - diffs, - ((forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match { - case (Some(userName), Some(repositoryName)) => - getRepository(userName, repositoryName) match { - case Some(x) => x.repository :: getForkedRepositories(userName, repositoryName) - case None => getForkedRepositories(userName, repositoryName) - } - case _ => - forkedRepository.repository :: getForkedRepositories(forkedRepository.owner, forkedRepository.name) - }).map { repository => - (repository.userName, repository.repositoryName, repository.defaultBranch) - }, - commits.flatten - .map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)) - .flatten - .toList, - originId, - forkedId, - oldId.getName, - newId.getName, - getContentTemplate(originRepository, "PULL_REQUEST_TEMPLATE"), - forkedRepository, - originRepository, - forkedRepository, - hasDeveloperRole(originRepository.owner, originRepository.name, context.loginAccount), - getAssignableUserNames(originRepository.owner, originRepository.name), - getMilestones(originRepository.owner, originRepository.name), - getPriorities(originRepository.owner, originRepository.name), - getLabels(originRepository.owner, originRepository.name) - ) - } - case (oldId, newId) => - redirect( - s"/${forkedRepository.owner}/${forkedRepository.name}/compare/" + - s"${originOwner}:${oldId.map(_ => originId).getOrElse(originRepository.repository.defaultBranch)}..." + - s"${forkedOwner}:${newId.map(_ => forkedId).getOrElse(forkedRepository.repository.defaultBranch)}" - ) + val title = if (commits.flatten.length == 1) { + commits.flatten.head.shortMessage + } else { + val text = forkedId.replaceAll("[\\-_]", " ") + text.substring(0, 1).toUpperCase + text.substring(1) } + + html.compare( + title, + commits, + diffs, + ((forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match { + case (Some(userName), Some(repositoryName)) => + getRepository(userName, repositoryName) match { + case Some(x) => x.repository :: getForkedRepositories(userName, repositoryName) + case None => getForkedRepositories(userName, repositoryName) + } + case _ => + forkedRepository.repository :: getForkedRepositories(forkedRepository.owner, forkedRepository.name) + }).map { repository => + (repository.userName, repository.repositoryName, repository.defaultBranch) + }, + commits.flatten + .map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)) + .flatten + .toList, + originId, + forkedId, + oldId.getName, + newId.getName, + getContentTemplate(originRepository, "PULL_REQUEST_TEMPLATE"), + forkedRepository, + originRepository, + forkedRepository, + hasDeveloperRole(originRepository.owner, originRepository.name, context.loginAccount), + getAssignableUserNames(originRepository.owner, originRepository.name), + getMilestones(originRepository.owner, originRepository.name), + getPriorities(originRepository.owner, originRepository.name), + getLabels(originRepository.owner, originRepository.name) + ) + } + case (oldId, newId) => + redirect( + s"/${forkedRepository.owner}/${forkedRepository.name}/compare/" + + s"${originOwner}:${oldId.map(_ => originId).getOrElse(originRepository.repository.defaultBranch)}..." + + s"${forkedOwner}:${newId.map(_ => forkedId).getOrElse(forkedRepository.repository.defaultBranch)}" + ) + } }) getOrElse NotFound() }) @@ -653,20 +624,6 @@ html.proposals(proposedBranches, targetRepository, repository) }) - /** - * Parses branch identifier and extracts owner and branch name as tuple. - * - * - "owner:branch" to ("owner", "branch") - * - "branch" to ("defaultOwner", "branch") - */ - private def parseCompareIdentifier(value: String, defaultOwner: String): (String, String) = - if (value.contains(':')) { - val array = value.split(":") - (array(0), array(1)) - } else { - (defaultOwner, value) - } - private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) = defining(repository.owner, repository.name) { case (owner, repoName) => diff --git a/src/main/scala/gitbucket/core/controller/api/ApiIssueControllerBase.scala b/src/main/scala/gitbucket/core/controller/api/ApiIssueControllerBase.scala index 6fe0632..12f44be 100644 --- a/src/main/scala/gitbucket/core/controller/api/ApiIssueControllerBase.scala +++ b/src/main/scala/gitbucket/core/controller/api/ApiIssueControllerBase.scala @@ -5,7 +5,7 @@ import gitbucket.core.service.{AccountService, IssueCreationService, IssuesService, MilestonesService} import gitbucket.core.service.IssuesService.IssueSearchCondition import gitbucket.core.service.PullRequestService.PullRequestLimit -import gitbucket.core.util.{ReadableUsersAuthenticator, ReferrerAuthenticator, RepositoryName} +import gitbucket.core.util.{ReadableUsersAuthenticator, ReferrerAuthenticator, RepositoryName, UsersAuthenticator} import gitbucket.core.util.Implicits._ trait ApiIssueControllerBase extends ControllerBase { @@ -18,6 +18,7 @@ /* * i. List issues * https://developer.github.com/v3/issues/#list-issues + * requested: 1743 */ /* diff --git a/src/main/scala/gitbucket/core/controller/api/ApiIssueLabelControllerBase.scala b/src/main/scala/gitbucket/core/controller/api/ApiIssueLabelControllerBase.scala index 0f9e4cc..307a4db 100644 --- a/src/main/scala/gitbucket/core/controller/api/ApiIssueLabelControllerBase.scala +++ b/src/main/scala/gitbucket/core/controller/api/ApiIssueLabelControllerBase.scala @@ -109,26 +109,84 @@ * vi. List labels on an issue * https://developer.github.com/v3/issues/labels/#list-labels-on-an-issue */ + get("/api/v3/repos/:owner/:repository/issues/:id/labels")(referrersOnly { repository => + JsonFormat(getIssueLabels(repository.owner, repository.name, params("id").toInt).map { l => + ApiLabel(l, RepositoryName(repository.owner, repository.name)) + }) + }) /* * vii. Add labels to an issue * https://developer.github.com/v3/issues/labels/#add-labels-to-an-issue */ + post("/api/v3/repos/:owner/:repository/issues/:id/labels")(writableUsersOnly { repository => + JsonFormat(for { + data <- extractFromJsonBody[Seq[String]]; + issueId <- params("id").toIntOpt + } yield { + data.map { labelName => + val label = getLabel(repository.owner, repository.name, labelName).getOrElse( + getLabel( + repository.owner, + repository.name, + createLabel(repository.owner, repository.name, labelName) + ).get + ) + registerIssueLabel(repository.owner, repository.name, issueId, label.labelId, true) + ApiLabel(label, RepositoryName(repository.owner, repository.name)) + } + }) + }) /* * viii. Remove a label from an issue * https://developer.github.com/v3/issues/labels/#remove-a-label-from-an-issue */ + delete("/api/v3/repos/:owner/:repository/issues/:id/labels/:name")(writableUsersOnly { repository => + val issueId = params("id").toInt + val labelName = params("name") + getLabel(repository.owner, repository.name, labelName) match { + case Some(label) => + deleteIssueLabel(repository.owner, repository.name, issueId, label.labelId, true) + JsonFormat(Seq(label)) + case None => + NotFound() + } + }) /* * ix. Replace all labels for an issue * https://developer.github.com/v3/issues/labels/#replace-all-labels-for-an-issue */ + put("/api/v3/repos/:owner/:repository/issues/:id/labels")(writableUsersOnly { repository => + JsonFormat(for { + data <- extractFromJsonBody[Seq[String]]; + issueId <- params("id").toIntOpt + } yield { + deleteAllIssueLabels(repository.owner, repository.name, issueId, true) + data.map { labelName => + val label = getLabel(repository.owner, repository.name, labelName).getOrElse( + getLabel( + repository.owner, + repository.name, + createLabel(repository.owner, repository.name, labelName) + ).get + ) + registerIssueLabel(repository.owner, repository.name, issueId, label.labelId, true) + ApiLabel(label, RepositoryName(repository.owner, repository.name)) + } + }) + }) /* * x. Remove all labels from an issue * https://developer.github.com/v3/issues/labels/#remove-all-labels-from-an-issue */ + delete("/api/v3/repos/:owner/:repository/issues/:id/labels")(writableUsersOnly { repository => + val issueId = params("id").toInt + deleteAllIssueLabels(repository.owner, repository.name, issueId, true) + NoContent() + }) /* * xi Get labels for every issue in a milestone diff --git a/src/main/scala/gitbucket/core/controller/api/ApiOrganizationControllerBase.scala b/src/main/scala/gitbucket/core/controller/api/ApiOrganizationControllerBase.scala index 0b64c9f..85ca93c 100644 --- a/src/main/scala/gitbucket/core/controller/api/ApiOrganizationControllerBase.scala +++ b/src/main/scala/gitbucket/core/controller/api/ApiOrganizationControllerBase.scala @@ -1,26 +1,36 @@ package gitbucket.core.controller.api -import gitbucket.core.api.{ApiRepository, ApiUser, JsonFormat} +import gitbucket.core.api.{ApiGroup, ApiRepository, ApiUser, JsonFormat} import gitbucket.core.controller.ControllerBase import gitbucket.core.service.{AccountService, RepositoryService} import gitbucket.core.util.Implicits._ +import gitbucket.core.util.UsersAuthenticator trait ApiOrganizationControllerBase extends ControllerBase { - self: RepositoryService with AccountService => + self: RepositoryService with AccountService with UsersAuthenticator => /* * i. List your organizations * https://developer.github.com/v3/orgs/#list-your-organizations */ + get("/api/v3/user/orgs")(usersOnly { + JsonFormat(getGroupsByUserName(context.loginAccount.get.userName).flatMap(getAccountByUserName(_)).map(ApiGroup(_))) + }) /* * ii. List all organizations * https://developer.github.com/v3/orgs/#list-all-organizations */ + get("/api/v3/organizations") { + JsonFormat(getAllUsers(false, true).filter(a => a.isGroupAccount).map(ApiGroup(_))) + } /* * iii. List user organizations * https://developer.github.com/v3/orgs/#list-user-organizations */ + get("/api/v3/users/:userName/orgs") { + JsonFormat(getGroupsByUserName(params("userName")).flatMap(getAccountByUserName(_)).map(ApiGroup(_))) + } /** * iv. Get an organization @@ -28,7 +38,26 @@ */ get("/api/v3/orgs/:groupName") { getAccountByUserName(params("groupName")).filter(account => account.isGroupAccount).map { account => - JsonFormat(ApiUser(account)) + JsonFormat(ApiGroup(account)) } getOrElse NotFound() } + + /* + * v. Edit an organization + * https://developer.github.com/v3/orgs/#edit-an-organization + */ + + /* + * ghe: i. Create an organization + * https://developer.github.com/enterprise/2.14/v3/enterprise-admin/orgs/#create-an-organization + */ + + /* + * ghe: ii. Rename an organization + * https://developer.github.com/enterprise/2.14/v3/enterprise-admin/orgs/#rename-an-organization + */ + + /* + * should implement delete an organization API? + */ } diff --git a/src/main/scala/gitbucket/core/controller/api/ApiPullRequestControllerBase.scala b/src/main/scala/gitbucket/core/controller/api/ApiPullRequestControllerBase.scala index fe66183..13cc75d 100644 --- a/src/main/scala/gitbucket/core/controller/api/ApiPullRequestControllerBase.scala +++ b/src/main/scala/gitbucket/core/controller/api/ApiPullRequestControllerBase.scala @@ -2,19 +2,28 @@ import gitbucket.core.api._ import gitbucket.core.controller.ControllerBase import gitbucket.core.model.{Account, Issue, PullRequest, Repository} -import gitbucket.core.service.{AccountService, IssuesService, PullRequestService, RepositoryService} +import gitbucket.core.service._ import gitbucket.core.service.IssuesService.IssueSearchCondition import gitbucket.core.service.PullRequestService.PullRequestLimit import gitbucket.core.util.Directory.getRepositoryDir import gitbucket.core.util.Implicits._ import gitbucket.core.util.JGitUtil.CommitInfo import gitbucket.core.util.SyntaxSugars.using -import gitbucket.core.util.{ReferrerAuthenticator, RepositoryName} +import gitbucket.core.util._ import org.eclipse.jgit.api.Git +import org.scalatra.NoContent + import scala.collection.JavaConverters._ trait ApiPullRequestControllerBase extends ControllerBase { - self: AccountService with IssuesService with PullRequestService with RepositoryService with ReferrerAuthenticator => + self: AccountService + with IssuesService + with PullRequestService + with RepositoryService + with MergeService + with ReferrerAuthenticator + with ReadableUsersAuthenticator + with WritableUsersAuthenticator => /* * i. Link Relations @@ -25,9 +34,6 @@ * ii. List pull requests * https://developer.github.com/v3/pulls/#list-pull-requests */ - /** - * 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 @@ -62,45 +68,92 @@ * iii. Get a single pull request * https://developer.github.com/v3/pulls/#get-a-single-pull-request */ - /** - * 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.empty - ) - baseOwner <- users.get(repository.owner) - headOwner <- users.get(pullRequest.requestUserName) - issueUser <- users.get(issue.openedUserName) - assignee = issue.assignedUserName.flatMap { userName => - getAccountByUserName(userName, false) - } - headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName) } yield { - JsonFormat( - ApiPullRequest( - issue = issue, - pullRequest = pullRequest, - headRepo = ApiRepository(headRepo, ApiUser(headOwner)), - baseRepo = ApiRepository(repository, ApiUser(baseOwner)), - user = ApiUser(issueUser), - labels = getIssueLabels(repository.owner, repository.name, issue.issueId) - .map(ApiLabel(_, RepositoryName(repository))), - assignee = assignee.map(ApiUser.apply), - mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId) - ) - ) + JsonFormat(getApiPullRequest(repository, issueId)) }) getOrElse NotFound() }) /* * iv. Create a pull request * https://developer.github.com/v3/pulls/#create-a-pull-request + * requested #1843 */ + post("/api/v3/repos/:owner/:repository/pulls")(readableUsersOnly { repository => + (for { + data <- extractFromJsonBody[Either[CreateAPullRequest, CreateAPullRequestAlt]] + } yield { + data match { + case Left(createPullReq) => + val (reqOwner, reqBranch) = parseCompareIdentifier(createPullReq.head, repository.owner) + getRepository(reqOwner, repository.name) + .flatMap { + forkedRepository => + getPullRequestCommitFromTo(repository, forkedRepository, createPullReq.base, reqBranch) match { + case (Some(commitIdFrom), Some(commitIdTo)) => + val issueId = insertIssue( + owner = repository.owner, + repository = repository.name, + loginUser = context.loginAccount.get.userName, + title = createPullReq.title, + content = createPullReq.body, + assignedUserName = None, + milestoneId = None, + priorityId = None, + isPullRequest = true + ) + + createPullRequest( + originUserName = repository.owner, + originRepositoryName = repository.name, + issueId = issueId, + originBranch = createPullReq.base, + requestUserName = reqOwner, + requestRepositoryName = repository.name, + requestBranch = reqBranch, + commitIdFrom = commitIdFrom.getName, + commitIdTo = commitIdTo.getName + ) + getApiPullRequest(repository, issueId).map(JsonFormat(_)) + case _ => + None + } + } + .getOrElse { + NotFound() + } + case Right(createPullReqAlt) => + val (reqOwner, reqBranch) = parseCompareIdentifier(createPullReqAlt.head, repository.owner) + getRepository(reqOwner, repository.name) + .flatMap { + forkedRepository => + getPullRequestCommitFromTo(repository, forkedRepository, createPullReqAlt.base, reqBranch) match { + case (Some(commitIdFrom), Some(commitIdTo)) => + changeIssueToPullRequest(repository.owner, repository.name, createPullReqAlt.issue) + createPullRequest( + originUserName = repository.owner, + originRepositoryName = repository.name, + issueId = createPullReqAlt.issue, + originBranch = createPullReqAlt.base, + requestUserName = reqOwner, + requestRepositoryName = repository.name, + requestBranch = reqBranch, + commitIdFrom = commitIdFrom.getName, + commitIdTo = commitIdTo.getName + ) + getApiPullRequest(repository, createPullReqAlt.issue).map(JsonFormat(_)) + case _ => + None + } + } + .getOrElse { + NotFound() + } + } + }) + }) /* * v. Update a pull request @@ -111,9 +164,6 @@ * vi. List commits on a pull request * https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request */ - /** - * 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 @@ -148,6 +198,18 @@ * viii. Get if a pull request has been merged * https://developer.github.com/v3/pulls/#get-if-a-pull-request-has-been-merged */ + get("/api/v3/repos/:owner/:repository/pulls/:id/merge")(referrersOnly { repository => + (for { + issueId <- params("id").toIntOpt + (issue, pullReq) <- getPullRequest(repository.owner, repository.name, issueId) + } yield { + if (checkConflict(repository.owner, repository.name, pullReq.branch, issueId).isDefined) { + NoContent + } else { + NotFound + } + }).getOrElse(NotFound) + }) /* * ix. Merge a pull request (Merge Button) @@ -155,7 +217,36 @@ */ /* - * x. Labels, assignees, and milestones - * https://developer.github.com/v3/pulls/#labels-assignees-and-milestones - */ + * x. Labels, assignees, and milestones + * https://developer.github.com/v3/pulls/#labels-assignees-and-milestones + */ + + private def getApiPullRequest(repository: RepositoryService.RepositoryInfo, issueId: Int): Option[ApiPullRequest] = { + for { + (issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId) + users = getAccountsByUserNames( + Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), + Set.empty + ) + baseOwner <- users.get(repository.owner) + headOwner <- users.get(pullRequest.requestUserName) + issueUser <- users.get(issue.openedUserName) + assignee = issue.assignedUserName.flatMap { userName => + getAccountByUserName(userName, false) + } + headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName) + } yield { + ApiPullRequest( + issue = issue, + pullRequest = pullRequest, + headRepo = ApiRepository(headRepo, ApiUser(headOwner)), + baseRepo = ApiRepository(repository, ApiUser(baseOwner)), + user = ApiUser(issueUser), + labels = getIssueLabels(repository.owner, repository.name, issue.issueId) + .map(ApiLabel(_, RepositoryName(repository))), + assignee = assignee.map(ApiUser.apply), + mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId) + ) + } + } } diff --git a/src/main/scala/gitbucket/core/controller/api/ApiRepositoryCollaboratorControllerBase.scala b/src/main/scala/gitbucket/core/controller/api/ApiRepositoryCollaboratorControllerBase.scala index 402b7a3..f8fe1e5 100644 --- a/src/main/scala/gitbucket/core/controller/api/ApiRepositoryCollaboratorControllerBase.scala +++ b/src/main/scala/gitbucket/core/controller/api/ApiRepositoryCollaboratorControllerBase.scala @@ -1,12 +1,13 @@ package gitbucket.core.controller.api -import gitbucket.core.api.{ApiUser, JsonFormat} +import gitbucket.core.api.{AddACollaborator, ApiUser, JsonFormat} import gitbucket.core.controller.ControllerBase import gitbucket.core.service.{AccountService, RepositoryService} import gitbucket.core.util.Implicits._ -import gitbucket.core.util.ReferrerAuthenticator +import gitbucket.core.util.{OwnerAuthenticator, ReferrerAuthenticator} +import org.scalatra.NoContent trait ApiRepositoryCollaboratorControllerBase extends ControllerBase { - self: RepositoryService with AccountService with ReferrerAuthenticator => + self: RepositoryService with AccountService with ReferrerAuthenticator with OwnerAuthenticator => /* * i. List collaborators @@ -31,10 +32,24 @@ /* * iv. Add user as a collaborator * https://developer.github.com/v3/repos/collaborators/#add-user-as-a-collaborator + * requested #1586 */ + put("/api/v3/repos/:owner/:repository/collaborators/:userName")(ownerOnly { repository => + for { + data <- extractFromJsonBody[AddACollaborator] + } yield { + addCollaborator(repository.owner, repository.name, params("userName"), data.role) + NoContent() + } + }) /* - * v. Remove user as a collaborator - * https://developer.github.com/v3/repos/collaborators/#remove-user-as-a-collaborator - */ + * v. Remove user as a collaborator + * https://developer.github.com/v3/repos/collaborators/#remove-user-as-a-collaborator + * requested #1586 + */ + delete("/api/v3/repos/:owner/:repository/collaborators/:userName")(ownerOnly { repository => + removeCollaborator(repository.owner, repository.name, params("userName")) + NoContent() + }) } diff --git a/src/main/scala/gitbucket/core/controller/api/ApiRepositoryContentsControllerBase.scala b/src/main/scala/gitbucket/core/controller/api/ApiRepositoryContentsControllerBase.scala index c1d128b..d0dae95 100644 --- a/src/main/scala/gitbucket/core/controller/api/ApiRepositoryContentsControllerBase.scala +++ b/src/main/scala/gitbucket/core/controller/api/ApiRepositoryContentsControllerBase.scala @@ -103,16 +103,19 @@ /* * iii. Create a file * https://developer.github.com/v3/repos/contents/#create-a-file + * requested #2112 */ /* * iv. Update a file * https://developer.github.com/v3/repos/contents/#update-a-file + * requested #2112 */ /* * v. Delete a file * https://developer.github.com/v3/repos/contents/#delete-a-file + * should be implemented */ /* diff --git a/src/main/scala/gitbucket/core/controller/api/ApiUserControllerBase.scala b/src/main/scala/gitbucket/core/controller/api/ApiUserControllerBase.scala index 7c596fd..4b69621 100644 --- a/src/main/scala/gitbucket/core/controller/api/ApiUserControllerBase.scala +++ b/src/main/scala/gitbucket/core/controller/api/ApiUserControllerBase.scala @@ -1,11 +1,13 @@ package gitbucket.core.controller.api -import gitbucket.core.api.{ApiUser, JsonFormat} +import gitbucket.core.api.{ApiUser, CreateAUser, JsonFormat, UpdateAUser} import gitbucket.core.controller.ControllerBase import gitbucket.core.service.{AccountService, RepositoryService} +import gitbucket.core.util.{AdminAuthenticator, UsersAuthenticator} import gitbucket.core.util.Implicits._ +import org.scalatra.NoContent trait ApiUserControllerBase extends ControllerBase { - self: RepositoryService with AccountService => + self: RepositoryService with AccountService with AdminAuthenticator with UsersAuthenticator => /** * i. Get a single user @@ -32,6 +34,18 @@ * iii. Update the authenticated user * https://developer.github.com/v3/users/#update-the-authenticated-user */ + patch("/api/v3/user")(usersOnly { + (for { + data <- extractFromJsonBody[UpdateAUser] + } yield { + val loginAccount = context.loginAccount.get + val updatedAccount = loginAccount.copy( + mailAddress = data.email.getOrElse(loginAccount.mailAddress) + ) + updateAccount(updatedAccount) + JsonFormat(ApiUser(updatedAccount)) + }) + }) /* * iv. Get contextual information about a user @@ -39,8 +53,62 @@ */ /* - * v. Get all users - * https://developer.github.com/v3/users/#get-all-users - */ + * v. Get all users + * https://developer.github.com/v3/users/#get-all-users + */ + get("/api/v3/users") { + JsonFormat(getAllUsers(false, false).map(a => ApiUser(a))) + } + /* + * ghe: i. Create a new user + * https://developer.github.com/enterprise/2.14/v3/enterprise-admin/users/#create-a-new-user + */ + post("/api/v3/admin/users")(adminOnly { + for { + data <- extractFromJsonBody[CreateAUser] + } yield { + val user = createAccount( + data.login, + data.password, + data.fullName.getOrElse(data.login), + data.email, + data.isAdmin.getOrElse(false), + data.description, + data.url + ) + JsonFormat(ApiUser(user)) + } + }) + + /* + * ghe: vii. Suspend a user + * https://developer.github.com/enterprise/2.14/v3/enterprise-admin/users/#suspend-a-user + */ + put("/api/v3/users/:userName/suspended")(adminOnly { + val userName = params("userName") + getAccountByUserName(userName) match { + case Some(targetAccount) => + removeUserRelatedData(userName) + updateAccount(targetAccount.copy(isRemoved = true)) + NoContent() + case None => + NotFound() + } + }) + + /* + * ghe: vii. Unsuspend a user + * https://developer.github.com/enterprise/2.14/v3/enterprise-admin/users/#unsuspend-a-user + */ + delete("/api/v3/users/:userName/suspended")(adminOnly { + val userName = params("userName") + getAccountByUserName(userName) match { + case Some(targetAccount) => + updateAccount(targetAccount.copy(isRemoved = false)) + NoContent() + case None => + NotFound() + } + }) } diff --git a/src/main/scala/gitbucket/core/service/AccountService.scala b/src/main/scala/gitbucket/core/service/AccountService.scala index b8bcba4..151edb0 100644 --- a/src/main/scala/gitbucket/core/service/AccountService.scala +++ b/src/main/scala/gitbucket/core/service/AccountService.scala @@ -164,8 +164,8 @@ isAdmin: Boolean, description: Option[String], url: Option[String] - )(implicit s: Session): Unit = - Accounts insert Account( + )(implicit s: Session): Account = { + val account = Account( userName = userName, password = password, fullName = fullName, @@ -180,6 +180,9 @@ isRemoved = false, description = description ) + Accounts insert account + account + } def suspendAccount(account: Account)(implicit s: Session): Unit = { // Remove from GROUP_MEMBER and COLLABORATOR diff --git a/src/main/scala/gitbucket/core/service/IssuesService.scala b/src/main/scala/gitbucket/core/service/IssuesService.scala index 86103d2..29665bd 100644 --- a/src/main/scala/gitbucket/core/service/IssuesService.scala +++ b/src/main/scala/gitbucket/core/service/IssuesService.scala @@ -475,6 +475,25 @@ IssueLabels filter (_.byPrimaryKey(owner, repository, issueId, labelId)) delete } + def deleteAllIssueLabels(owner: String, repository: String, issueId: Int, insertComment: Boolean = false)( + implicit context: Context, + s: Session + ): Int = { + if (insertComment) { + IssueComments insert IssueComment( + userName = owner, + repositoryName = repository, + issueId = issueId, + action = "delete_label", + commentedUserName = context.loginAccount.map(_.userName).getOrElse("Unknown user"), + content = "All labels", + registeredDate = currentDate, + updatedDate = currentDate + ) + } + IssueLabels filter (_.byIssue(owner, repository, issueId)) delete + } + def createComment( owner: String, repository: String, @@ -507,6 +526,15 @@ .update(title, content, currentDate) } + def changeIssueToPullRequest(owner: String, repository: String, issueId: Int)(implicit s: Session) = { + Issues + .filter(_.byPrimaryKey(owner, repository, issueId)) + .map { t => + t.pullRequest + } + .update(true) + } + def updateAssignedUserName( owner: String, repository: String, diff --git a/src/main/scala/gitbucket/core/service/LabelsService.scala b/src/main/scala/gitbucket/core/service/LabelsService.scala index 4bbd5d8..a0f7dc8 100644 --- a/src/main/scala/gitbucket/core/service/LabelsService.scala +++ b/src/main/scala/gitbucket/core/service/LabelsService.scala @@ -3,6 +3,7 @@ import gitbucket.core.model.Label import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile.profile.blockingApi._ +import gitbucket.core.util.StringUtil trait LabelsService { @@ -24,6 +25,11 @@ ) } + def createLabel(owner: String, repository: String, labelName: String)(implicit s: Session): Int = { + val color = StringUtil.md5(labelName).substring(0, 6) + createLabel(owner, repository, labelName, color) + } + def updateLabel(owner: String, repository: String, labelId: Int, labelName: String, color: String)( implicit s: Session ): Unit = diff --git a/src/main/scala/gitbucket/core/service/PullRequestService.scala b/src/main/scala/gitbucket/core/service/PullRequestService.scala index 09d2552..185955c 100644 --- a/src/main/scala/gitbucket/core/service/PullRequestService.scala +++ b/src/main/scala/gitbucket/core/service/PullRequestService.scala @@ -4,6 +4,7 @@ import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile.profile.blockingApi._ import difflib.{Delta, DiffUtils} +import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.api.JsonFormat import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.Directory._ @@ -14,6 +15,7 @@ import gitbucket.core.view import gitbucket.core.view.helpers import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.ObjectId import scala.collection.JavaConverters._ @@ -399,6 +401,58 @@ updateClosed(owner, repository, pull.issueId, true) } + /** + * Parses branch identifier and extracts owner and branch name as tuple. + * + * - "owner:branch" to ("owner", "branch") + * - "branch" to ("defaultOwner", "branch") + */ + def parseCompareIdentifier(value: String, defaultOwner: String): (String, String) = + if (value.contains(':')) { + val array = value.split(":") + (array(0), array(1)) + } else { + (defaultOwner, value) + } + + def getPullRequestCommitFromTo( + originRepository: RepositoryInfo, + forkedRepository: RepositoryInfo, + originId: String, + forkedId: String + ): (Option[ObjectId], Option[ObjectId]) = { + using( + Git.open(getRepositoryDir(originRepository.owner, originRepository.name)), + Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name)) + ) { + case (oldGit, newGit) => + if (originRepository.branchList.contains(originId)) { + val forkedId2 = + forkedRepository.tags.collectFirst { case x if x.name == forkedId => x.id }.getOrElse(forkedId) + + val originId2 = JGitUtil.getForkedCommitId( + oldGit, + newGit, + originRepository.owner, + originRepository.name, + originId, + forkedRepository.owner, + forkedRepository.name, + forkedId2 + ) + + (Option(oldGit.getRepository.resolve(originId2)), Option(newGit.getRepository.resolve(forkedId2))) + + } else { + val originId2 = + originRepository.tags.collectFirst { case x if x.name == originId => x.id }.getOrElse(originId) + val forkedId2 = + forkedRepository.tags.collectFirst { case x if x.name == forkedId => x.id }.getOrElse(forkedId) + + (Option(oldGit.getRepository.resolve(originId2)), Option(newGit.getRepository.resolve(forkedId2))) + } + } + } } object PullRequestService { diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala index d784291..2f152db 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala @@ -621,6 +621,14 @@ Collaborators insert Collaborator(userName, repositoryName, collaboratorName, role) /** + * Remove specified collaborator from the repository. + */ + def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String)( + implicit s: Session + ): Unit = + Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete + + /** * Remove all collaborators from the repository. */ def removeCollaborators(userName: String, repositoryName: String)(implicit s: Session): Unit =