Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Create / Update / Delete requests #390

Merged
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 @@ -108,11 +108,11 @@ class HttpClient[F[_]: Sync](client: Client[F]) {

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 @@ -126,6 +126,19 @@ class HttpClient[F[_]: Sync](client: Client[F]) {
.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._
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this dependency transitive?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's used like this in the AuthInterpreter also. I'm not sure where it's from. Shall we pin it to the build?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes might be a good idea 👍


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