Skip to content

Commit

Permalink
feat: add Create / Update / Delete requests (#390)
Browse files Browse the repository at this point in the history
* feat: add Create / Update / Delete requests

* test: finish unit tests

* fix: use Array[Byte] and handle base64 requirement

* doc: update mdocs

* Update github4s/src/main/scala/github4s/algebras/Repositories.scala

Co-Authored-By: Ben Fradet <benjamin.fradet@gmail.com>

* Update github4s/src/main/scala/github4s/algebras/Repositories.scala

Co-Authored-By: Ben Fradet <benjamin.fradet@gmail.com>

* Update github4s/src/main/scala/github4s/algebras/Repositories.scala

Co-Authored-By: Ben Fradet <benjamin.fradet@gmail.com>

* fix: review comments and correct JSON modeling.

* Update version.sbt

don't change the version.

Co-authored-by: Ben Fradet <benjamin.fradet@gmail.com>
  • Loading branch information
kalexmills and BenFradet committed Apr 1, 2020
1 parent 7c7f8ed commit e1dc143
Show file tree
Hide file tree
Showing 10 changed files with 415 additions and 2 deletions.
82 changes: 82 additions & 0 deletions docs/docs/repository.md
Expand Up @@ -19,6 +19,9 @@ with Github4s, you can interact with:
- [List commits on a repository](#list-commits-on-a-repository)
- [Contents](#contents)
- [Get contents](#get-contents)
- [Create a File](#create-a-file)
- [Update a File](#update-a-file)
- [Delete a File](#delete-a-file)
- [Releases](#releases)
- [Create a release](#create-a-release)
- [Statuses](#statuses)
Expand Down Expand Up @@ -266,6 +269,85 @@ The `result` on the right is the corresponding [NonEmptyList[Content]][repositor
See [the API doc](https://developer.github.com/v3/repos/contents/#get-contents) for full
reference.

### Create a File

This method creates a new file in an existing repository.

You can create a new file using `createFile`, it takes as arguments:

- the repository coordinates (`owner` and `name` of the repository).
- `path`: The path of the new file to be created, *without* a leading slash.
- `message`: The message to use for creating the commit.
- `content`: The content of the new file, as an array of bytes.
- `branch`: The branch to add the commit to. If omitted, this defaults to the repository's default branch.
- `committer`: An optional committer to associate with the commit. If omitted, the authenticated user's information is used for the commit.
- `author`: An optional author to associate with the commit. If omitted, the committer is used (if present).

To create a file:
```scala mdoc:compile-only
val getContents = gh.repos.createFile("47degrees", "github4s", "new-file.txt", "create a new file", "file contents".getBytes)

getContents.unsafeRunSync().result match {
case Left(e) => println(s"We could not create your file because ${e.getMessage}")
case Right(r) => println(r)
}
```

See [the API doc](https://developer.github.com/v3/repos/contents/#create-or-update-a-file) for full reference.

### Update a File

This method updates an existing file in a repository.

You can create a new file using `createFile`, it takes as arguments:

- the repository coordinates (`owner` and `name` of the repository).
- `path`: The path of the new file to be created, *without* a leading slash.
- `message`: The message to use for creating the commit.
- `content`: The content of the new file, as an array of bytes.
- `sha`: The blob SHA of the file being replaced. (This is returned as part of a `getContents` call). GitHub uses this value to perform optimistic locking. If the file has been updated since, the update call will fail.
- `branch`: The branch to add the commit to. If omitted, this defaults to the repository's default branch.
- `committer`: An optional committer to associate with the commit. If omitted, the authenticated user's information is used for the commit.
- `author`: An optional author to associate with the commit. If omitted, the committer is used (if present).

To create a file:
```scala mdoc:compile-only
val getContents = gh.repos.updateFile("47degrees", "github4s", "README.md", "A terser README.", "You read me right.".getBytes,"a52d080d2cf85e08bfcb441b437d3982398e8f8f6a58388f55d6b6cf51cb5365")

getContents.unsafeRunSync().result match {
case Left(e) => println(s"We could not update your file because ${e.getMessage}")
case Right(r) => println(r)
}
```

See [the API doc](https://developer.github.com/v3/repos/contents/#create-or-update-a-file) for full reference.

### Delete a File

This method deletes an existing file in a repository.

You can create a new file using `deleteFile`, it takes as arguments:

- the repository coordinates (`owner` and `name` of the repository).
- `path`: The path of the new file to be created, *without* a leading slash.
- `message`: The message to use for creating the commit.
- `sha`: The blob SHA of the file being replaced. (This is returned as part of a `getContents` call). GitHub uses this value to perform optimistic locking. If the file has been updated since, the update call will fail.
- `branch`: The branch to add the commit to. If omitted, this defaults to the repository's default branch.
- `committer`: An optional committer to associate with the commit. If omitted, the authenticated user's information is used for the commit.
- `author`: An optional author to associate with the commit. If omitted, the committer is used (if present).

To create a file:
```scala mdoc:compile-only
val getContents = gh.repos.deleteFile("47degrees", "github4s", "README.md", "Actually, we don't need a README.", "a52d080d2cf85e08bfcb441b437d3982398e8f8f6a58388f55d6b6cf51cb5365")

getContents.unsafeRunSync().result match {
case Left(e) => println(s"We could not delete this file because ${e.getMessage}")
case Right(r) => println(r)
}
```

See [the API doc](https://developer.github.com/v3/repos/contents/#delete-a-file) for full reference.

## Releases

### Create a release
Expand Down
2 changes: 2 additions & 0 deletions github4s/src/main/scala/github4s/Decoders.scala
Expand Up @@ -241,6 +241,8 @@ object Decoders {
}
}

implicit val decoderWriteFileResponse: Decoder[WriteFileResponse] =
deriveDecoder[WriteFileResponse]
implicit val decoderPullRequestFile: Decoder[PullRequestFile] = deriveDecoder[PullRequestFile]
implicit val decoderPullRequestReview: Decoder[PullRequestReview] =
deriveDecoder[PullRequestReview]
Expand Down
5 changes: 5 additions & 0 deletions github4s/src/main/scala/github4s/Encoders.scala
Expand Up @@ -43,6 +43,11 @@ object Encoders {
)
}

implicit val encoderCommiter: Encoder[Committer] = deriveEncoder[Committer]
implicit val encoderDeleteFileRequest: Encoder[DeleteFileRequest] =
deriveEncoder[DeleteFileRequest]
implicit val encoderWriteFileContentRequest: Encoder[WriteFileRequest] =
deriveEncoder[WriteFileRequest]
implicit val encoderCreateReferenceRequest: Encoder[CreateReferenceRequest] =
deriveEncoder[CreateReferenceRequest]
implicit val encoderNewCommitRequest: Encoder[NewCommitRequest] = deriveEncoder[NewCommitRequest]
Expand Down
77 changes: 77 additions & 0 deletions github4s/src/main/scala/github4s/algebras/Repositories.scala
Expand Up @@ -103,6 +103,83 @@ trait Repositories[F[_]] {
headers: Map[String, String] = Map()
): F[GHResponse[NonEmptyList[Content]]]

/**
* Creates a new file in a repository.
*
* @param owner of the repo
* @param repo name of the repo
* @param path the content path
* @param content content in bytes, as they should be written to GitHub.
* @param message the message to be included in the commit.
* @param branch the branch name (defaults to the repository's default branch)
* @param committer object containing information about the committer (filled in with authenticated user information if omitted)
* @param author object containing information about the author (filled in with committer information if omitted)
* @return GHResponse[WriteFileResponse] with details about the content created and the commit
*/
def createFile(
owner: String,
repo: String,
path: String,
message: String,
content: Array[Byte],
branch: Option[String] = None,
committer: Option[Committer] = None,
author: Option[Committer] = None,
headers: Map[String, String] = Map()
): F[GHResponse[WriteFileResponse]]

/**
* Updates an existing file in a repository.
*
* @param owner of the repo
* @param repo name of the repo
* @param path the content path
* @param message the message to be included in the commit.
* @param content the content of the file as it should be written to GitHub
* @param sha the blob sha of the file being replaced.
* @param branch the branch name (defaults to the repository's default branch)
* @param committer object containing information about the committer (filled in with authenticated user information if omitted)
* @param author object containing information about the author (filled in with committer information if omitted)
* @return GHResponse[WriteFileResponse] with details about the content updated and the commit
*/
def updateFile(
owner: String,
repo: String,
path: String,
message: String,
content: Array[Byte],
sha: String,
branch: Option[String] = None,
committer: Option[Committer] = None,
author: Option[Committer] = None,
headers: Map[String, String] = Map()
): F[GHResponse[WriteFileResponse]]

/**
* Deletes a file in a particular repo, resulting in a new commit.
*
* @param owner of the repo
* @param repo name of the repo
* @param path the content path
* @param message the message to be included in the commit.
* @param sha the blob sha of the file being replaced.
* @param branch the branch name (defaults to the repository's default branch)
* @param committer object containing information about the committer (filled in with authenticated user information if omitted)
* @param author object containing information about the author (filled in with committer information if omitted)
* @return GHResponse[WriteFileResponse] with no content and details about the commit which was added.
*/
def deleteFile(
owner: String,
repo: String,
path: String,
message: String,
sha: String,
branch: Option[String] = None,
committer: Option[Committer] = None,
author: Option[Committer] = None,
headers: Map[String, String] = Map()
): F[GHResponse[WriteFileResponse]]

/**
* Retrieve the list of commits for a particular repo
*
Expand Down
36 changes: 36 additions & 0 deletions github4s/src/main/scala/github4s/domain/Repository.scala
Expand Up @@ -166,6 +166,42 @@ case class CombinedStatus(
statuses: List[Status],
repository: StatusRepository
)

case class WriteFileRequest(
message: String,
content: String,
sha: Option[String] = None,
branch: Option[String] = None,
committer: Option[Committer] = None,
author: Option[Committer] = None
)

case class DeleteFileRequest(
message: String,
sha: String,
branch: Option[String] = None,
committer: Option[Committer] = None,
author: Option[Committer] = None
)

case class WriteResponseCommit(
sha: String,
url: String,
html_url: String,
author: Option[Committer],
committer: Option[Committer],
message: String
)

case class WriteFileResponse(
content: Option[Content],
commit: WriteResponseCommit
)

case class Committer(
name: String,
email: String
)
object RepoUrlKeys {

val forks_url = "forks_url"
Expand Down
17 changes: 15 additions & 2 deletions github4s/src/main/scala/github4s/http/HttpClient.scala
Expand Up @@ -107,11 +107,11 @@ class HttpClient[F[_]: Sync](client: Client[F], val config: GithubConfig) {

def delete(
accessToken: Option[String] = None,
method: String,
url: String,
headers: Map[String, String] = Map.empty
): F[GHResponse[Unit]] =
run[Unit, Unit](
RequestBuilder(buildURL(method)).deleteMethod.withHeaders(headers).withAuth(accessToken)
RequestBuilder(buildURL(url)).deleteMethod.withHeaders(headers).withAuth(accessToken)
)

def deleteWithResponse[Res: Decoder](
Expand All @@ -125,6 +125,19 @@ class HttpClient[F[_]: Sync](client: Client[F], val config: GithubConfig) {
.withHeaders(headers)
)

def deleteWithBody[Req: Encoder, Res: Decoder](
accessToken: Option[String] = None,
url: String,
headers: Map[String, String] = Map.empty,
data: Req
): F[GHResponse[Res]] =
run[Req, Res](
RequestBuilder(buildURL(url)).deleteMethod
.withAuth(accessToken)
.withHeaders(headers)
.withData(data)
)

val defaultPagination = Pagination(1, 1000)
val defaultPage: Int = 1
val defaultPerPage: Int = 30
Expand Down
Expand Up @@ -23,6 +23,7 @@ import github4s.GithubResponses.GHResponse
import github4s.domain._
import github4s.Decoders._
import github4s.Encoders._
import com.github.marklister.base64.Base64._

class RepositoriesInterpreter[F[_]](implicit client: HttpClient[F], accessToken: Option[String])
extends Repositories[F] {
Expand Down Expand Up @@ -75,6 +76,61 @@ class RepositoriesInterpreter[F[_]](implicit client: HttpClient[F], accessToken:
ref.fold(Map.empty[String, String])(r => Map("ref" -> r))
)

override def createFile(
owner: String,
repo: String,
path: String,
message: String,
content: Array[Byte],
branch: Option[String],
committer: Option[Committer],
author: Option[Committer],
headers: Map[String, String] = Map()
): F[GHResponse[WriteFileResponse]] =
client.put[WriteFileRequest, WriteFileResponse](
accessToken,
s"repos/$owner/$repo/contents/$path",
headers,
WriteFileRequest(message, content.toBase64, None, branch, committer, author)
)

override def updateFile(
owner: String,
repo: String,
path: String,
message: String,
content: Array[Byte],
sha: String,
branch: Option[String],
committer: Option[Committer],
author: Option[Committer],
headers: Map[String, String] = Map()
): F[GHResponse[WriteFileResponse]] =
client.put[WriteFileRequest, WriteFileResponse](
accessToken,
s"repos/$owner/$repo/contents/$path",
headers,
WriteFileRequest(message, content.toBase64, Some(sha), branch, committer, author)
)

override def deleteFile(
owner: String,
repo: String,
path: String,
message: String,
sha: String,
branch: Option[String],
committer: Option[Committer],
author: Option[Committer],
headers: Map[String, String] = Map()
): F[GHResponse[WriteFileResponse]] =
client.deleteWithBody[DeleteFileRequest, WriteFileResponse](
accessToken,
s"repos/$owner/$repo/contents/$path",
headers,
DeleteFileRequest(message, sha, branch, committer, author)
)

override def listCommits(
owner: String,
repo: String,
Expand Down

0 comments on commit e1dc143

Please sign in to comment.