diff --git a/github4s/src/main/scala/github4s/Decoders.scala b/github4s/src/main/scala/github4s/Decoders.scala index 7b720612..671d933f 100644 --- a/github4s/src/main/scala/github4s/Decoders.scala +++ b/github4s/src/main/scala/github4s/Decoders.scala @@ -18,6 +18,7 @@ package github4s import cats.data.NonEmptyList import cats.syntax.all._ +import github4s.domain.RepoUrlKeys.{CommitComparisonResponse, FileComparison} import github4s.domain._ import io.circe.Decoder.Result import io.circe._ @@ -241,6 +242,8 @@ object Decoders { ) ) + implicit val decoderFileComparison: Decoder[FileComparison] = deriveDecoder[FileComparison] + implicit val decoderCreatePullRequestData: Decoder[CreatePullRequestData] = deriveDecoder[CreatePullRequestData] implicit val decoderCreatePullRequestIssue: Decoder[CreatePullRequestIssue] = @@ -265,6 +268,8 @@ object Decoders { implicit val decoderNewTagRequest: Decoder[NewTagRequest] = deriveDecoder[NewTagRequest] implicit val decoderNewTreeRequest: Decoder[NewTreeRequest] = deriveDecoder[NewTreeRequest] implicit val decoderNewCommitRequest: Decoder[NewCommitRequest] = deriveDecoder[NewCommitRequest] + implicit val decoderBranchUpdateRequest: Decoder[BranchUpdateRequest] = + deriveDecoder[BranchUpdateRequest] implicit def decodeNonEmptyList[T](implicit D: Decoder[T]): Decoder[NonEmptyList[T]] = { @@ -367,4 +372,9 @@ object Decoders { implicit val decoderEditGistRequest: Decoder[EditGistRequest] = deriveDecoder[EditGistRequest] implicit val decoderEditIssueRequest: Decoder[EditIssueRequest] = deriveDecoder[EditIssueRequest] implicit val decoderMilestoneData: Decoder[MilestoneData] = deriveDecoder[MilestoneData] + + implicit val decodeBranchUpdateResponse: Decoder[BranchUpdateResponse] = + deriveDecoder[BranchUpdateResponse] + implicit val decodeCommitComparisonResponse: Decoder[CommitComparisonResponse] = + deriveDecoder[CommitComparisonResponse] } diff --git a/github4s/src/main/scala/github4s/Encoders.scala b/github4s/src/main/scala/github4s/Encoders.scala index 7d7dbaae..4392b96f 100644 --- a/github4s/src/main/scala/github4s/Encoders.scala +++ b/github4s/src/main/scala/github4s/Encoders.scala @@ -16,6 +16,7 @@ package github4s +import github4s.domain.RepoUrlKeys.{CommitComparisonResponse, FileComparison} import github4s.domain._ import io.circe._ import io.circe.generic.semiauto.deriveEncoder @@ -182,6 +183,9 @@ object Encoders { deriveEncoder[NewReleaseRequest] implicit val encoderNewStatusRequest: Encoder[NewStatusRequest] = deriveEncoder[NewStatusRequest] implicit val encoderMilestoneData: Encoder[MilestoneData] = deriveEncoder[MilestoneData] + implicit val encodeBranchUpdateRequest: Encoder[BranchUpdateRequest] = + deriveEncoder[BranchUpdateRequest] + implicit val encoderCreateReviewComment: Encoder[CreateReviewComment] = deriveEncoder[CreateReviewComment] implicit val encodeNewPullRequestReview: Encoder[CreatePRReviewRequest] = @@ -237,4 +241,9 @@ object Encoders { implicit val encoderUser: Encoder[User] = deriveEncoder[User] implicit val encoderComment: Encoder[Comment] = deriveEncoder[Comment] implicit val encoderMilestone: Encoder[Milestone] = deriveEncoder[Milestone] + implicit val encodeBranchUpdateResponse: Encoder[BranchUpdateResponse] = + deriveEncoder[BranchUpdateResponse] + implicit val encodeFileComparison: Encoder[FileComparison] = deriveEncoder[FileComparison] + implicit val encodeCommitComparisonResponse: Encoder[CommitComparisonResponse] = + deriveEncoder[CommitComparisonResponse] } diff --git a/github4s/src/main/scala/github4s/algebras/PullRequests.scala b/github4s/src/main/scala/github4s/algebras/PullRequests.scala index fae833f8..f75d9b20 100644 --- a/github4s/src/main/scala/github4s/algebras/PullRequests.scala +++ b/github4s/src/main/scala/github4s/algebras/PullRequests.scala @@ -179,4 +179,15 @@ trait PullRequests[F[_]] { reviewers: ReviewersRequest, headers: Map[String, String] = Map() ): F[GHResponse[PullRequest]] + + /** + * This is an experimental API and could be changed or removed + */ + def updateBranch( + owner: String, + repo: String, + pullRequest: Int, + expectedHeadSha: Option[String] = None, + headers: Map[String, String] = Map() + ): F[GHResponse[BranchUpdateResponse]] } diff --git a/github4s/src/main/scala/github4s/algebras/Repositories.scala b/github4s/src/main/scala/github4s/algebras/Repositories.scala index aa6e3c6f..0d5fe64e 100644 --- a/github4s/src/main/scala/github4s/algebras/Repositories.scala +++ b/github4s/src/main/scala/github4s/algebras/Repositories.scala @@ -18,6 +18,7 @@ package github4s.algebras import cats.data.NonEmptyList import github4s.GHResponse +import github4s.domain.RepoUrlKeys.CommitComparisonResponse import github4s.domain._ trait Repositories[F[_]] { @@ -228,6 +229,24 @@ trait Repositories[F[_]] { headers: Map[String, String] = Map() ): F[GHResponse[List[Commit]]] + /** + * Compare any two commits in the same repository + * + * @param owner of the repo + * @param repo name of the repo + * @param commitSha commit to compare against base + * @param baseSha the base to compare against + * @param headers optional user headers to include in the request + * @return GhResponse[CommitComparisonResponse] comparison result + */ + def compareCommits( + owner: String, + repo: String, + commitSha: String, + baseSha: String, + headers: Map[String, String] = Map() + ): F[GHResponse[CommitComparisonResponse]] + /** * Retrieve list of branches for a repo * diff --git a/github4s/src/main/scala/github4s/domain/PullRequest.scala b/github4s/src/main/scala/github4s/domain/PullRequest.scala index a8a2ae3f..aa87d49c 100644 --- a/github4s/src/main/scala/github4s/domain/PullRequest.scala +++ b/github4s/src/main/scala/github4s/domain/PullRequest.scala @@ -151,3 +151,6 @@ final case class ReviewersResponse( users: List[User], teams: List[Team] ) + +final case class BranchUpdateRequest(expected_head_sha: Option[String]) +final case class BranchUpdateResponse(message: String, url: String) diff --git a/github4s/src/main/scala/github4s/domain/Repository.scala b/github4s/src/main/scala/github4s/domain/Repository.scala index 8d1dbe37..114f3936 100644 --- a/github4s/src/main/scala/github4s/domain/Repository.scala +++ b/github4s/src/main/scala/github4s/domain/Repository.scala @@ -349,4 +349,33 @@ object RepoUrlKeys { deployments_url ) + final case class CommitComparisonResponse( + status: String, + ahead_by: Int, + behind_by: Int, + total_commits: Int, + url: Option[String] = None, + html_url: Option[String] = None, + permalink_url: Option[String] = None, + diff_url: Option[String] = None, + patch_url: Option[String] = None, + base_commit: Option[Commit] = None, + merge_base_commit: Option[Commit] = None, + commits: Seq[Commit] = Seq.empty, + files: Seq[FileComparison] = Seq.empty + ) + + final case class FileComparison( + sha: String, + filename: String, + status: String, + additions: Int, + deletions: Int, + changes: Int, + blob_url: String, + raw_url: String, + contents_url: String, + patch: String + ) + } diff --git a/github4s/src/main/scala/github4s/interpreters/PullRequestsInterpreter.scala b/github4s/src/main/scala/github4s/interpreters/PullRequestsInterpreter.scala index 55475b90..6535fafc 100644 --- a/github4s/src/main/scala/github4s/interpreters/PullRequestsInterpreter.scala +++ b/github4s/src/main/scala/github4s/interpreters/PullRequestsInterpreter.scala @@ -160,4 +160,17 @@ class PullRequestsInterpreter[F[_]](implicit client: HttpClient[F]) extends Pull headers, reviewers ) + + override def updateBranch( + owner: String, + repo: String, + pullRequest: Int, + expectedHeadSha: Option[String] = None, + headers: Map[String, String] = Map() + ): F[GHResponse[BranchUpdateResponse]] = + client.put[BranchUpdateRequest, BranchUpdateResponse]( + s"repos/$owner/$repo/pulls/$pullRequest/update-branch", + headers ++ Seq("Accept" -> "application/vnd.github.lydian-preview+json"), + BranchUpdateRequest(expectedHeadSha) + ) } diff --git a/github4s/src/main/scala/github4s/interpreters/RepositoriesInterpreter.scala b/github4s/src/main/scala/github4s/interpreters/RepositoriesInterpreter.scala index 39cd21fa..3afd7b30 100644 --- a/github4s/src/main/scala/github4s/interpreters/RepositoriesInterpreter.scala +++ b/github4s/src/main/scala/github4s/interpreters/RepositoriesInterpreter.scala @@ -19,13 +19,14 @@ package github4s.interpreters import cats.Functor import cats.data.NonEmptyList import cats.syntax.functor._ -import github4s.internal.Base64._ import github4s.Decoders._ import github4s.Encoders._ import github4s.GHResponse import github4s.algebras.Repositories +import github4s.domain.RepoUrlKeys.CommitComparisonResponse import github4s.domain._ import github4s.http.HttpClient +import github4s.internal.Base64._ class RepositoriesInterpreter[F[_]: Functor](implicit client: HttpClient[F] @@ -169,6 +170,18 @@ class RepositoriesInterpreter[F[_]: Functor](implicit pagination ) + override def compareCommits( + owner: String, + repo: String, + commitSha: String, + baseSha: String, + headers: Map[String, String] = Map() + ): F[GHResponse[CommitComparisonResponse]] = + client.get[CommitComparisonResponse]( + s"repos/$owner/$repo/compare/$baseSha...$commitSha", + headers + ) + override def listBranches( owner: String, repo: String, @@ -344,5 +357,4 @@ class RepositoriesInterpreter[F[_]: Functor](implicit response.copy(result = Right(false)) case Left(error) => GHResponse(Left(error), response.statusCode, response.headers) } - } diff --git a/github4s/src/test/scala-2/github4s/unit/EncoderDecoderSpec.scala b/github4s/src/test/scala-2/github4s/unit/EncoderDecoderSpec.scala index e3cf6379..45157606 100644 --- a/github4s/src/test/scala-2/github4s/unit/EncoderDecoderSpec.scala +++ b/github4s/src/test/scala-2/github4s/unit/EncoderDecoderSpec.scala @@ -5,6 +5,7 @@ import com.eed3si9n.expecty.Expecty import github4s.ArbitraryDerivation import github4s.Decoders._ import github4s.Encoders._ +import github4s.domain.RepoUrlKeys.CommitComparisonResponse import github4s.domain._ import io.circe.{Decoder, Encoder, Printer} import org.scalacheck.fortyseven.GenInstances._ @@ -133,6 +134,9 @@ class EncoderDecoderSpec extends AnyFlatSpec with ScalaCheckPropertyChecks { test[Repository] test[ReviewersRequest] test[ReviewersResponse] + test[BranchUpdateRequest] + test[BranchUpdateResponse] + test[CommitComparisonResponse] test[SearchIssuesResult] test[SearchReposResult] test[Stargazer] diff --git a/github4s/src/test/scala/github4s/integration/PullRequestsSpec.scala b/github4s/src/test/scala/github4s/integration/PullRequestsSpec.scala index 3673340b..e637d0d1 100644 --- a/github4s/src/test/scala/github4s/integration/PullRequestsSpec.scala +++ b/github4s/src/test/scala/github4s/integration/PullRequestsSpec.scala @@ -356,4 +356,21 @@ trait PullRequestsSpec extends BaseIntegrationSpec { ) removeReviewersResponse.statusCode shouldBe okStatusCode } + + "PullRequests >> Update Branch" should "merge target branch's head into selected" taggedAs Integration in { + val response = clientResource + .use { client => + Github[IO](client, accessToken).pullRequests + .updateBranch( + validRepoOwner, + validRepoName, + validPullRequestNumber, + headers = headerUserAgent + ) + } + .unsafeRunSync() + + testIsRight[BranchUpdateResponse](response) + response.statusCode shouldBe okStatusCode + } } diff --git a/github4s/src/test/scala/github4s/integration/ReposSpec.scala b/github4s/src/test/scala/github4s/integration/ReposSpec.scala index 9c5af34b..0abe9cdb 100644 --- a/github4s/src/test/scala/github4s/integration/ReposSpec.scala +++ b/github4s/src/test/scala/github4s/integration/ReposSpec.scala @@ -20,6 +20,7 @@ import cats.data.NonEmptyList import cats.effect.{IO, Resource} import cats.implicits._ import github4s.GHError.{NotFoundError, UnauthorizedError} +import github4s.domain.RepoUrlKeys.CommitComparisonResponse import github4s.domain._ import github4s.utils.{BaseIntegrationSpec, Integration} import github4s.{GHResponse, Github} @@ -471,6 +472,24 @@ trait ReposSpec extends BaseIntegrationSpec { response.statusCode shouldBe okStatusCode } + "Repos >> Compare" should "compare against the base" taggedAs Integration in { + val response = clientResource + .use { client => + Github[IO](client, accessToken).repos + .compareCommits(validRepoOwner, validRepoName, validCommitSha, validBase) + } + .unsafeRunSync() + + testIsRight[CommitComparisonResponse]( + response, + { r => + r.status shouldBe "behind" + r.behind_by should be > 0 + } + ) + response.statusCode shouldBe okStatusCode + } + it should "successfully return results when a valid repo is provided using / syntax" taggedAs Integration in { val response = clientResource .use { client => diff --git a/github4s/src/test/scala/github4s/unit/PullRequestsSpec.scala b/github4s/src/test/scala/github4s/unit/PullRequestsSpec.scala index af95dd2c..2b3d2ab5 100644 --- a/github4s/src/test/scala/github4s/unit/PullRequestsSpec.scala +++ b/github4s/src/test/scala/github4s/unit/PullRequestsSpec.scala @@ -261,4 +261,25 @@ class PullRequestsSpec extends BaseSpec { .shouldNotFail } + "PullRequests.updateBranch" should "call to httpClient.put with the right parameters" in { + + implicit val httpClientMock: HttpClient[IO] = + httpClientMockPut[BranchUpdateRequest, BranchUpdateResponse]( + url = s"repos/$validRepoOwner/$validRepoName/pulls/$validPullRequestNumber/update-branch", + req = BranchUpdateRequest(None), + response = IO.pure(validBranchUpdateResponse) + ) + + val pullRequests = new PullRequestsInterpreter[IO] + + pullRequests + .updateBranch( + validRepoOwner, + validRepoName, + validPullRequestNumber, + None, + headerUserAgent + ) + .shouldNotFail + } } diff --git a/github4s/src/test/scala/github4s/unit/ReposSpec.scala b/github4s/src/test/scala/github4s/unit/ReposSpec.scala index 1b7abdd5..ad04d515 100644 --- a/github4s/src/test/scala/github4s/unit/ReposSpec.scala +++ b/github4s/src/test/scala/github4s/unit/ReposSpec.scala @@ -20,6 +20,7 @@ import cats.data.NonEmptyList import cats.effect.IO import github4s.Decoders._ import github4s.Encoders._ +import github4s.domain.RepoUrlKeys.CommitComparisonResponse import github4s.domain._ import github4s.http.HttpClient import github4s.internal.Base64._ @@ -434,4 +435,25 @@ class ReposSpec extends BaseSpec { repos.searchRepos("", validSearchParams, None, headerUserAgent).shouldNotFail } + + "Repos.compare" should "call to httpClient.get with the right parameters" in { + + implicit val httpClientMock: HttpClient[IO] = httpClientMockGet[CommitComparisonResponse]( + url = s"repos/$validRepoOwner/$validRepoName/compare/$validCommitSha...$validMergeCommitSha", + response = IO.pure(validCommitComparisonResponse) + ) + + val repos = new RepositoriesInterpreter[IO] + + repos + .compareCommits( + validRepoOwner, + validRepoName, + validCommitSha, + validMergeCommitSha, + headerUserAgent + ) + .shouldNotFail + } + } diff --git a/github4s/src/test/scala/github4s/utils/TestData.scala b/github4s/src/test/scala/github4s/utils/TestData.scala index 8ea9a4a1..309e2e1d 100644 --- a/github4s/src/test/scala/github4s/utils/TestData.scala +++ b/github4s/src/test/scala/github4s/utils/TestData.scala @@ -16,8 +16,9 @@ package github4s.utils -import java.util.UUID +import github4s.domain.RepoUrlKeys.CommitComparisonResponse +import java.util.UUID import github4s.internal.Base64._ import github4s.domain._ @@ -489,6 +490,14 @@ trait TestData { val validRequestedReviewersResponse = ReviewersResponse(List(user), List(team)) + val validBranchUpdateResponse = + BranchUpdateResponse( + "Updating pull request branch.", + s"https://github.com/repos/$validRepoOwner/$validRepoName/pulls/$validPullRequestNumber" + ) + + val validCommitComparisonResponse = CommitComparisonResponse("behind", 1, 2, 100) + val validProjectId = 4115271L val invalidProjectId = 11111L diff --git a/microsite/docs/pull_request.md b/microsite/docs/pull_request.md index 5229502d..a1ab80b8 100644 --- a/microsite/docs/pull_request.md +++ b/microsite/docs/pull_request.md @@ -14,6 +14,7 @@ with Github4s, you can interact with: - [List pull requests](#list-pull-requests) - [List the files in a pull request](#list-the-files-in-a-pull-request) - [Create a pull request](#create-a-pull-request) + - [Update branch](#update-a-pull-request-branch) - [Reviews](#reviews) - [List reviews](#list-pull-request-reviews) - [Get a review](#get-an-individual-review) @@ -158,6 +159,32 @@ createPullRequestIssue.flatMap(_.result match { See [the API doc](https://developer.github.com/v3/pulls/#create-a-pull-request) for full reference. +### Update a pull request branch + +Merges the base HEAD into your pull request branch. +Note that this is an experimental API, meaning github could stop supporting it at any time or change in an incompatible way. + +Accepts these parameters: + + - the repository coordinates (`owner` and `name` of the repository). + - `pullRequest`: integer id of you pr. + - `expectedHeadSha`: The expected SHA of the pull request's HEAD ref for an optional check on github's side. + +```scala mdoc:compile-only +import github4s.domain.BranchUpdateResponse + +val updatePullRequestBranch = gh.pullRequests.updateBranch( + "47deg", + "github4s", + 567) +updatePullRequestBranch.flatMap(_.result match { + case Left(e) => IO.println(s"Something went wrong: ${e.getMessage}") + case Right(r) => IO.println(r) +}) +``` + +See [the API doc](https://developer.github.com/v3/pulls/#update-a-pull-request-branch) for full reference. + ## Reviews ### List pull request reviews diff --git a/microsite/docs/repository.md b/microsite/docs/repository.md index 311bef2f..3be972e5 100644 --- a/microsite/docs/repository.md +++ b/microsite/docs/repository.md @@ -20,6 +20,7 @@ with Github4s, you can interact with: - [Get repository permissions for a user](#get-repository-permissions-for-a-user) - [Commits](#commits) - [List commits on a repository](#list-commits-on-a-repository) + - [Compare commits on a repository](#compare-commits-on-a-repository) - [Contents](#contents) - [Get contents](#get-contents) - [Create a File](#create-a-file) @@ -263,6 +264,34 @@ The `result` on the right is the corresponding [List[Commit]][repository-scala]. See [the API doc](https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository) for full reference. +### Compare commits on a repository + +You can compare two commits using `compareCommits`, it takes as arguments: + +- the repository coordinates (`owner` and `name` of the repository). +- `commitSha` or branch to identify the commit you want to check. +- `baseSha`: or branch you're comparing the commitSha against + +To compare commits: + +```scala mdoc:compile-only +val compareCommits = + gh.repos.compareCommits( + "47deg", + "github4s", + "d3b048c1f500ee5450e5d7b3d1921ed3e7645891", + "main") +compareCommits.flatMap(_.result match { + case Left(e) => IO.println(s"Something went wrong: ${e.getMessage}") + case Right(r) => IO.println(r) +}) +``` + +The `result` on the right is the corresponding [List[CommitComparisonResponse]][repository-scala]. + +See [the API doc](https://developer.github.com/v3/repos/#compare-two-commits) for full +reference. + ## Branches ### List branches on a repository