├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.sbt ├── data ├── labels.json └── milestones.json ├── gh-shell.md ├── project ├── build.properties └── plugins.sbt ├── src ├── main │ └── scala │ │ └── codecheck │ │ └── github │ │ ├── api │ │ ├── APIResult.scala │ │ ├── DebugHandler.scala │ │ ├── GitHubAPI.scala │ │ ├── OAuthAPI.scala │ │ └── RepositoryAPI.scala │ │ ├── app │ │ ├── Command.scala │ │ ├── CommandRunner.scala │ │ ├── CommandSetting.scala │ │ ├── Main.scala │ │ └── commands │ │ │ ├── ChangeRepositoryCommand.scala │ │ │ ├── IssueCommand.scala │ │ │ ├── LabelCommand.scala │ │ │ ├── MilestoneCommand.scala │ │ │ └── RepositoryCommand.scala │ │ ├── events │ │ ├── DefaultEvent.scala │ │ ├── GitHubEvent.scala │ │ ├── IssueCommentEvent.scala │ │ ├── IssueEvent.scala │ │ ├── PullRequestEvent.scala │ │ ├── PullRequestReviewEvent.scala │ │ └── PushEvent.scala │ │ ├── exceptions │ │ ├── GitHubAPIException.scala │ │ ├── NotFoundException.scala │ │ ├── OAuthAPIException.scala │ │ ├── PermissionDeniedException.scala │ │ └── UnauthorizedException.scala │ │ ├── models │ │ ├── AbstractInput.scala │ │ ├── AbstractJson.scala │ │ ├── AccessToken.scala │ │ ├── Branch.scala │ │ ├── Collaborator.scala │ │ ├── Comment.scala │ │ ├── ErrorResponse.scala │ │ ├── Issue.scala │ │ ├── Label.scala │ │ ├── LanguageList.scala │ │ ├── Milestone.scala │ │ ├── OAuthErrorResponse.scala │ │ ├── Organization.scala │ │ ├── PullRequest.scala │ │ ├── PullRequestReview.scala │ │ ├── Repository.scala │ │ ├── ReviewRequest.scala │ │ ├── Search.scala │ │ ├── SortDirection.scala │ │ ├── Status.scala │ │ ├── User.scala │ │ └── Webhook.scala │ │ ├── operations │ │ ├── BranchOp.scala │ │ ├── CollaboratorOp.scala │ │ ├── IssueOp.scala │ │ ├── LabelOp.scala │ │ ├── MilestoneOp.scala │ │ ├── OrganizationOp.scala │ │ ├── PullRequestOp.scala │ │ ├── PullRequestReviewOp.scala │ │ ├── RepositoryOp.scala │ │ ├── SearchOp.scala │ │ ├── StatusOp.scala │ │ ├── UserOp.scala │ │ └── WebhookOp.scala │ │ ├── transport │ │ ├── CompletionHandler.scala │ │ ├── Request.scala │ │ ├── Response.scala │ │ ├── Transport.scala │ │ ├── asynchttp19 │ │ │ └── AsyncHttp19Transport.scala │ │ └── asynchttp20 │ │ │ └── AsyncHttp20Transport.scala │ │ └── utils │ │ ├── Json4s.scala │ │ ├── PrintList.scala │ │ └── ToDo.scala └── test │ └── scala │ ├── BranchOpSpec.scala │ ├── CollaboratorOpSpec.scala │ ├── Constants.scala │ ├── IssueOpSpec.scala │ ├── LabelOpSpec.scala │ ├── MilestoneOpSpec.scala │ ├── OraganizationOpSpec.scala │ ├── PullRequestOpSpec.scala │ ├── PullRequestReviewOpSpec.scala │ ├── RepositoryAPISpec.scala │ ├── RepositoryOpSpec.scala │ ├── SearchOpSpec.scala │ ├── StatusOpSpec.scala │ ├── UserOpSpec.scala │ ├── WebhookOpSpec.scala │ └── events │ ├── GitHubEventSpec.scala │ ├── IssueEventJson.scala │ ├── PullRequestEventJson.scala │ ├── PullRequestReviewEventJson.scala │ └── PushEventJson.scala └── test.md /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig (http://EditorConfig.org) 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | max_line_length = 80 12 | 13 | indent_style = space 14 | indent_size = 2 15 | 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | 20 | [COMMIT_EDITMSG] 21 | max_line_length = 0 22 | 23 | [*.scala] 24 | max_line_length = 120 25 | 26 | [*.php] 27 | indent_size = 4 28 | 29 | [*.html] 30 | 31 | [*.css] 32 | 33 | [*.js] 34 | 35 | # Reactjs 36 | [*.jsx] 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /RUNNING_PID 2 | /logs/ 3 | /project/*-shim.sbt 4 | /project/project/ 5 | /project/target/ 6 | /target/ 7 | env.sh 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | 3 | scala: 4 | - 2.12.10 5 | - 2.13.1 6 | 7 | script: 8 | - sbt ++$TRAVIS_SCALA_VERSION test:compile 9 | 10 | # Container-based build environment with faster boot times 11 | sudo: false 12 | 13 | jdk: 14 | - openjdk8 15 | - openjdk11 16 | 17 | before_cache: 18 | - find $HOME/.sbt -name "*.lock" | xargs rm 19 | - find $HOME/.ivy2/cache -name "ivydata-*.properties" | xargs rm 20 | cache: 21 | directories: 22 | - $HOME/.ivy2/cache 23 | - $HOME/.sbt/boot 24 | - $HOME/.sbt/launchers 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Givery Technology 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHubAPI for scala 2 | [![Build Status](https://travis-ci.org/code-check/github-api-scala.svg?branch=master)](https://travis-ci.org/code-check/github-api-scala) 3 | [![Latest version](https://index.scala-lang.org/code-check/github-api-scala/github-api/latest.svg?color=orange)](https://index.scala-lang.org/code-check/github-api-scala) 4 | 5 | GitHubAPI wrapper for scala 6 | 7 | ## Dependencies 8 | - joda-time 9 | - json4s 10 | - async-http-client 11 | 12 | ## Getting started 13 | 14 | To develop this, you have to get GitHub API Token. 15 | You can get it from [here](https://github.com/settings/applications). 16 | 17 | Add this library and an HTTP client library to your `build.sbt` file. 18 | Both versions 1.9 and 2.0 of the Asnyc HTTP Client are supported, so 19 | you choose. Ning's HTTP client will request a log binding, so we'll 20 | provide a basic one. 21 | 22 | ```scala 23 | libraryDependencies ++= Seq( 24 | "com.ning" % "async-http-client" % "1.9.40", 25 | "org.slf4j" % "slf4j-simple" % "1.7.26", 26 | "io.code-check" %% "github-api" % "0.3.0" 27 | ) 28 | ``` 29 | 30 | Using the code is as simple as starting an HTTP client instance, and 31 | providing it to the main API class. 32 | 33 | ```scala 34 | import com.ning.http.client.AsyncHttpClient 35 | 36 | import codecheck.github.transport.asynchttp19.AsyncHttp19Transport 37 | import codecheck.github.api.GitHubAPI 38 | import codecheck.github.models._ 39 | 40 | import org.slf4j.LoggerFactory 41 | 42 | import scala.concurrent.Await 43 | import scala.concurrent.duration._ 44 | import scala.concurrent.ExecutionContext.Implicits.global 45 | import scala.concurrent.Future 46 | 47 | object Main { 48 | 49 | val logger = LoggerFactory.getLogger(getClass) 50 | 51 | def main(args: Array[String]): Unit = { 52 | 53 | val githubToken = "a0b1c2d3e4f5g6h7ijklmnopqrst5u4v3w2x1y0z" 54 | 55 | val httpClient = new AsyncHttp19Transport(new AsyncHttpClient()) 56 | 57 | val githubApi = new GitHubAPI(githubToken, httpClient) 58 | 59 | val repoParams = 60 | RepositoryListOption( 61 | RepositoryListType.public, 62 | RepositorySort.updated, 63 | SortDirection.desc 64 | ) 65 | 66 | val repoListOp: Future[List[Repository]] = 67 | githubApi.listOwnRepositories(repoParams) 68 | 69 | val exec: Future[Unit] = 70 | for (repos <- repoListOp) 71 | yield 72 | for (repo <- repos) 73 | yield println(repoToJson(repo)) 74 | 75 | exec.onFailure { 76 | case e: Throwable => logger.error(e.toString) 77 | } 78 | 79 | Await.ready(exec, Duration.Inf) 80 | 81 | httpClient.close 82 | } 83 | 84 | /** Unsophisticated JSON serialization */ 85 | def repoToJson(repo: Repository): String = 86 | s"""{ 87 | | id: ${repo.id}, 88 | | name: "${repo.name}", 89 | | full_name: "${repo.full_name}", 90 | | url: "${repo.url}", 91 | | description: "${repo.description.getOrElse("")}", 92 | | owner: "${repo.owner.login}", 93 | | open_issues_count: ${repo.open_issues_count} 94 | |}""".stripMargin 95 | 96 | } 97 | ``` 98 | 99 | ## How to develop 100 | 101 | ``` bash 102 | export GITHUB_USER=[Your GitHub username] 103 | export GITHUB_REPO=[Your GitHub test repo] 104 | export GITHUB_TOKEN=[Your GitHub Token] 105 | git clone -o upstream git@github.com:code-check/github-api.git 106 | cd github-api 107 | sbt test 108 | ``` 109 | 110 | Currently, Java 8 is required to build this library. If you have 111 | multiple versions of Java installed on your system, set it to Java 8 112 | (also known as version 1.8). One method for choosing the Java version 113 | is to override the value of `JAVA_HOME` in the environment sbt runs. 114 | 115 | ``` 116 | $ env JAVA_HOME="$(/usr/libexec/java_home -v 1.8)" sbt 117 | ``` 118 | 119 | ## About models 120 | We don't aim to define all fields of JSON. 121 | Because these are too much and might be changed by GitHub. 122 | 123 | We just define the fields we need. 124 | 125 | ## License 126 | MIT -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | organization := "io.code-check" 2 | 3 | name := """github-api""" 4 | 5 | version := "0.3.1" 6 | 7 | scalaVersion := "2.13.1" 8 | 9 | crossScalaVersions := Seq("2.10.7", "2.11.12", "2.12.10", "2.13.1") 10 | 11 | description := "The GitHub API from Scala with Async HTTP Client (Netty)" 12 | 13 | licenses := Seq("MIT" -> url("http://opensource.org/licenses/MIT")) 14 | 15 | homepage := Some(url("http://github.com/code-check/github-api-scala")) 16 | 17 | scmInfo := Some( 18 | ScmInfo( 19 | url("https://github.com/code-check/github-api-scala"), 20 | "scm:git@github.com:code-check/github-api-scala.git" 21 | ) 22 | ) 23 | 24 | developers := List( 25 | Developer( 26 | id = "shunjikonishi", 27 | name = "Shunji Konishi", 28 | email = "@shunjikonishi", 29 | url = url("http://qiita.com/shunjikonishi") 30 | ) 31 | ) 32 | 33 | publishMavenStyle := true 34 | 35 | publishTo := { 36 | val nexus = "https://oss.sonatype.org/" 37 | if (isSnapshot.value) 38 | Some("snapshots" at nexus + "content/repositories/snapshots") 39 | else 40 | Some("releases" at nexus + "service/local/staging/deploy/maven2") 41 | } 42 | 43 | publishArtifact in Test := false 44 | 45 | pomIncludeRepository := { _ => false } 46 | 47 | // Change this to another test framework if you prefer 48 | libraryDependencies ++= Seq( 49 | "com.ning" % "async-http-client" % "1.9.40" % "provided", 50 | "org.asynchttpclient" % "async-http-client" % "2.0.39" % "provided", 51 | "org.json4s" %% "json4s-jackson" % "3.6.6", 52 | "org.json4s" %% "json4s-ext" % "3.6.6", 53 | "joda-time" % "joda-time" % "2.10.1", 54 | "com.github.scopt" %% "scopt" % "3.7.1", 55 | "org.slf4j" % "slf4j-nop" % "1.7.26" % "test", 56 | "org.scalatest" %% "scalatest" % "3.0.8" % "test" 57 | ) 58 | 59 | scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature") 60 | -------------------------------------------------------------------------------- /data/labels.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Fix me!", 4 | "color": "eb6420" 5 | }, 6 | { 7 | "name": "Review me!", 8 | "color": "fbca04" 9 | }, 10 | { 11 | "name": "Ship it!", 12 | "color": "207de5" 13 | }, 14 | { 15 | "name": "Done!", 16 | "color": "bfd4f2" 17 | } 18 | ] -------------------------------------------------------------------------------- /data/milestones.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Sprint4", 4 | "description": "2015/05/25 to 2015/06/07", 5 | "due_on": "2015-06-07" 6 | }, 7 | { 8 | "title": "Sprint5", 9 | "description": "2015/06/08 to 2015/06/21", 10 | "due_on": "2015-06-21" 11 | }, 12 | { 13 | "title": "Sprint6", 14 | "description": "2015/06/22 to 2015/07/05", 15 | "due_on": "2015-07-05" 16 | }, 17 | { 18 | "title": "Sprint7", 19 | "description": "2015/07/06 to 2015/07/19", 20 | "due_on": "2015-07-19" 21 | }, 22 | { 23 | "title": "Sprint8", 24 | "description": "2015/07/20 to 2015/08/02", 25 | "due_on": "2015-08-02" 26 | }, 27 | { 28 | "title": "Sprint9", 29 | "description": "2015/08/03 to 2015/08/16", 30 | "due_on": "2015-08-16" 31 | }, 32 | { 33 | "title": "Sprint10", 34 | "description": "2015/08/17 to 2015/08/30", 35 | "due_on": "2015-08-30" 36 | }, 37 | { 38 | "title": "Sprint11", 39 | "description": "2015/08/31 to 2015/09/13", 40 | "due_on": "2015-09-13" 41 | }, 42 | { 43 | "title": "Sprint12", 44 | "description": "2015/09/14 to 2015/09/27", 45 | "due_on": "2015-09-27" 46 | } 47 | ] 48 | -------------------------------------------------------------------------------- /gh-shell.md: -------------------------------------------------------------------------------- 1 | # gh-shell 2 | Sample application of github-api-scala 3 | 4 | It provides shell for GitHub. 5 | 6 | ## How to run 7 | This app is not released yet. 8 | So you have to build this with SBT. 9 | 10 | To run this, you can use GitHub username and password, or GITHUB_TOKEN 11 | 12 | ``` 13 | $git clone git@github.com:code-check/github-api-scala.git 14 | $cd github-api-scala 15 | $sbt "run -u USERNAME -p PASSWORD" 16 | 17 | or 18 | 19 | $export GITHUB_TOKEN=xxxxx 20 | $sbt run 21 | ``` 22 | 23 | ## How it works 24 | It works like shell. 25 | You can control GitHub with this. 26 | 27 | ``` bash 28 | $sbt "run -u shunjikonishi -p xxxxx" 29 | 30 | shunjikonishi>cr code-check test-repo 31 | shunjikonishi@code-check/test-repo>label list 32 | bug fc2929 33 | duplicate cccccc 34 | enhancement 84b6eb 35 | help wanted 159818 36 | invalid e6e6e6 37 | question cc317c 38 | wontfix ffffff 39 | 40 | shunjikonishi@code-check/test-repo>milestone merge data/milestones.json 41 | Create milestone Sprint1 42 | Create milestone Sprint3 43 | Create milestone Sprint4 44 | Create milestone Sprint2 45 | 46 | shunjikonishi@code-check/test-repo>milestone list 47 | 1 Sprint1 0/0 2015-04-26 48 | 4 Sprint2 0/0 2015-05-10 49 | 2 Sprint3 0/0 2015-05-24 50 | 3 Sprint4 0/0 2015-06-07 51 | shunjikonishi@code-check/test-repo>milestone rm 1 52 | Removed 1 53 | 54 | shunjikonishi@code-check/test-repo>milestone list -v 55 | 56 | --------------------------------------------- 57 | Sprint2 58 | number: 4 59 | due_on: 2015-05-10T00:00:00.000+09:00 60 | open : 0 61 | closed: 0 62 | 63 | 2015/04/27 to 2015/05/10 64 | 65 | --------------------------------------------- 66 | Sprint3 67 | number: 2 68 | due_on: 2015-05-24T00:00:00.000+09:00 69 | open : 0 70 | closed: 0 71 | 72 | From 2015/05/11 to 2015/05/24 73 | 74 | --------------------------------------------- 75 | Sprint4 76 | number: 3 77 | due_on: 2015-06-07T00:00:00.000+09:00 78 | open : 0 79 | closed: 0 80 | 81 | 2015/05/25 to 2015/06/07 82 | ``` 83 | 84 | ## Commands 85 | Currently following commands are implemented 86 | 87 | ### exit 88 | Exit from app. 89 | 90 | ### cr 91 | Change repository 92 | 93 | ``` 94 | $ cr code-check test-repo 95 | ``` 96 | 97 | If owner is omitted, the owner of current repository or logined user will be used. 98 | 99 | ### label 100 | Label control 101 | 102 | - list 103 | - add 104 | - update 105 | - rm 106 | - merge(Update multiple labels from file) 107 | 108 | ### milestone 109 | Milestone control 110 | 111 | - list 112 | - add 113 | - update 114 | - rm 115 | - merge(Update multiple milestones from file) 116 | 117 | ### repo 118 | Repository control 119 | 120 | - list 121 | 122 | ### issue 123 | Issue control 124 | 125 | - list 126 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.3.8 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.2-1") 2 | 3 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.6") 4 | 5 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/api/APIResult.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.api 2 | 3 | import org.json4s.JValue 4 | import org.json4s.jackson.JsonMethods 5 | 6 | case class APIResult(statusCode: Int, body: JValue) { 7 | override def toString = { 8 | "status = " + statusCode + "\n" + JsonMethods.pretty(body) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/api/DebugHandler.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.api 2 | 3 | import org.json4s.JValue 4 | import java.io.PrintStream 5 | 6 | trait DebugHandler { 7 | def onRequest(method: String, path: String, body: JValue): Unit 8 | def onResponse(status: Int, body: Option[String]): Unit 9 | } 10 | 11 | object NoneHandler extends DebugHandler { 12 | def onRequest(method: String, path: String, body: JValue): Unit = {} 13 | def onResponse(status: Int, body: Option[String]): Unit = {} 14 | } 15 | 16 | class PrintlnHandler(out: PrintStream = System.out) extends DebugHandler { 17 | def onRequest(method: String, path: String, body: JValue): Unit = { 18 | out.println(s"onRequest: $method $path $body") 19 | } 20 | def onResponse(status: Int, body: Option[String]): Unit = { 21 | out.println(s"onResponse: $status $body") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/api/GitHubAPI.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.api 2 | 3 | import scala.concurrent.Promise 4 | import scala.concurrent.Future 5 | import scala.concurrent.Await 6 | import scala.concurrent.duration.Duration 7 | import java.net.URLEncoder 8 | import java.util.Base64 9 | import org.json4s.JValue 10 | import org.json4s.JNothing 11 | import org.json4s.jackson.JsonMethods 12 | 13 | import codecheck.github.exceptions.PermissionDeniedException 14 | import codecheck.github.exceptions.NotFoundException 15 | import codecheck.github.exceptions.UnauthorizedException 16 | import codecheck.github.exceptions.GitHubAPIException 17 | import codecheck.github.operations._ 18 | import codecheck.github.models.User 19 | import codecheck.github.transport._ 20 | 21 | class GitHubAPI(token: String, client: Transport, tokenType: String = "token", debugHandler: DebugHandler = NoneHandler) extends UserOp 22 | with OrganizationOp 23 | with RepositoryOp 24 | with LabelOp 25 | with IssueOp 26 | with PullRequestOp 27 | with PullRequestReviewOp 28 | with MilestoneOp 29 | with StatusOp 30 | with WebhookOp 31 | with CollaboratorOp 32 | with BranchOp 33 | with SearchOp 34 | { 35 | 36 | private val endpoint = "https://api.github.com" 37 | 38 | private def parseJson(json: String) = { 39 | JsonMethods.parse(json) 40 | } 41 | 42 | protected def encode(s: String) = URLEncoder.encode(s, "utf-8").replaceAll("\\+", "%20") 43 | 44 | def exec(method: String, path: String, body: JValue = JNothing, fail404: Boolean = true): Future[APIResult] = { 45 | debugHandler.onRequest(method, path, body) 46 | val deferred = Promise[APIResult]() 47 | val url = endpoint + path 48 | val request = method match { 49 | case "GET" => client.prepareGet(url) 50 | case "PATCH" => client.preparePost(url) 51 | case "POST" => client.preparePost(url) 52 | case "PUT" => client.preparePut(url) 53 | case "DELETE" => client.prepareDelete(url) 54 | } 55 | if (body != JNothing) { 56 | request.setBody(JsonMethods.compact(body)) 57 | } 58 | request 59 | .setHeader("Authorization", s"$tokenType $token") 60 | .setHeader("Content-Type", "application/json") 61 | .setHeader("Accept", "application/vnd.github.v3+json") 62 | if (method == "PUT" && body == JNothing){ 63 | request 64 | .setHeader("Content-Length", "0") 65 | } 66 | request.execute(new CompletionHandler() { 67 | def onCompleted(res: Response) = { 68 | debugHandler.onResponse(res.getStatusCode, res.getResponseBody) 69 | val json = res.getResponseBody.filter(_.length > 0).map(parseJson(_)).getOrElse(JNothing) 70 | res.getStatusCode match { 71 | case 401 => 72 | deferred.failure(new UnauthorizedException(json)) 73 | case 403 => 74 | deferred.failure(new PermissionDeniedException(json)) 75 | case 422 => 76 | deferred.failure(new GitHubAPIException(json)) 77 | case 404 if fail404 => 78 | deferred.failure(new NotFoundException(json)) 79 | case _ => 80 | val result = APIResult(res.getStatusCode, json) 81 | deferred.success(result) 82 | } 83 | } 84 | def onThrowable(t: Throwable): Unit = { 85 | deferred.failure(t) 86 | } 87 | }) 88 | deferred.future 89 | } 90 | 91 | lazy val user = Await.result(getAuthenticatedUser, Duration.Inf) 92 | 93 | def repositoryAPI(owner: String, repo: String) = RepositoryAPI(this, owner, repo) 94 | 95 | def close = client.close 96 | 97 | def withDebugHandler(dh: DebugHandler): GitHubAPI = new GitHubAPI(token, client, tokenType, dh) 98 | } 99 | 100 | object GitHubAPI { 101 | 102 | def apply(token: String)(implicit client: Transport): GitHubAPI = new GitHubAPI(token, client) 103 | 104 | def apply(username: String, password: String)(implicit client: Transport): GitHubAPI = { 105 | val token = Base64.getEncoder.encodeToString((username + ":" + password).getBytes("utf-8")) 106 | new GitHubAPI(token, client, "Basic") 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/api/OAuthAPI.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.api 2 | 3 | import java.net.URLEncoder 4 | import scala.concurrent.Promise 5 | import scala.concurrent.Future 6 | import org.json4s.jackson.JsonMethods 7 | import org.json4s.DefaultFormats 8 | import java.util.UUID 9 | import codecheck.github.models.AccessToken 10 | import codecheck.github.exceptions.OAuthAPIException 11 | import codecheck.github.transport._ 12 | 13 | class OAuthAPI(clientId: String, clientSecret: String, redirectUri: String, client: Transport) { 14 | private implicit val format = DefaultFormats 15 | 16 | private val accessRequestUri = "https://github.com/login/oauth/authorize" 17 | private val tokenRequestUri = "https://github.com/login/oauth/access_token" 18 | 19 | def requestAccessUri(scope: String*) = { 20 | val params = Map[String, String]( 21 | "client_id" -> clientId, 22 | "redirect_uri" -> redirectUri, 23 | "scope" -> scope.mkString(","), 24 | "response_type" -> "token", 25 | "state" -> UUID.randomUUID.toString 26 | ) 27 | val query: String = params.map { case (k, v) => k +"="+ URLEncoder.encode(v, "utf-8") }.mkString("&") 28 | accessRequestUri +"?"+ query 29 | } 30 | 31 | def requestAccessUri(state: String, scope: Seq[String]) = { 32 | val params = Map[String, String]( 33 | "client_id" -> clientId, 34 | "redirect_uri" -> redirectUri, 35 | "scope" -> scope.mkString(","), 36 | "response_type" -> "token", 37 | "state" -> state 38 | ) 39 | val query: String = params.map { case (k, v) => k +"="+ URLEncoder.encode(v, "utf-8") }.mkString("&") 40 | accessRequestUri +"?"+ query 41 | } 42 | 43 | def requestToken(code: String): Future[AccessToken] = { 44 | val params: Map[String, String] = Map( 45 | "client_id" -> clientId, 46 | "client_secret" -> clientSecret, 47 | "code" -> code, 48 | "redirect_uri" -> redirectUri 49 | ) 50 | val request = client.preparePost(tokenRequestUri) 51 | .setHeader("Content-Type", "application/x-www-form-urlencoded") 52 | .setHeader("Accept", "application/json") 53 | .setFollowRedirect(true) 54 | params.foreach { case (k, v) => request.addFormParam(k, v) } 55 | 56 | val deferred = Promise[AccessToken]() 57 | request.execute(new CompletionHandler() { 58 | def onCompleted(res: Response) = { 59 | val body = res.getResponseBody.getOrElse("{\"error\": \"No response\"}") 60 | val json = JsonMethods.parse(body) 61 | (json \ "error").toOption match { 62 | case Some(_) => deferred.failure(new OAuthAPIException(json)) 63 | case None => deferred.success(AccessToken(json)) 64 | } 65 | } 66 | def onThrowable(t: Throwable): Unit = { 67 | deferred.failure(t) 68 | } 69 | }) 70 | deferred.future 71 | } 72 | } 73 | 74 | object OAuthAPI { 75 | def apply(clientId: String, clientSecret: String, redirectUri: String)(implicit client: Transport) = new OAuthAPI(clientId, clientSecret, redirectUri, client) 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/api/RepositoryAPI.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.api 2 | 3 | import scala.concurrent.Future 4 | import codecheck.github.models.Label 5 | import codecheck.github.models.LabelInput 6 | import codecheck.github.models.Issue 7 | import codecheck.github.models.IssueInput 8 | import codecheck.github.models.Milestone 9 | import codecheck.github.models.MilestoneInput 10 | import codecheck.github.models.MilestoneListOption 11 | import codecheck.github.models.PullRequest 12 | import codecheck.github.models.PullRequestInput 13 | import codecheck.github.models.PullRequestListOption 14 | import codecheck.github.models.ReviewRequest 15 | 16 | case class RepositoryAPI(api: GitHubAPI, owner: String, repo: String) { 17 | //IssueOp 18 | def getIssue(number: Long): Future[Option[Issue]] = 19 | api.getIssue(owner, repo, number) 20 | 21 | def editIssue(number: Long, params: IssueInput): Future[Issue] = 22 | api.editIssue(owner, repo, number, params) 23 | 24 | def assign(number: Long, assignee: String): Future[Issue] = 25 | api.assign(owner, repo, number, assignee) 26 | 27 | def unassign(number: Long): Future[Issue] = 28 | api.unassign(owner, repo, number) 29 | 30 | //LabelOp 31 | def addLabels(number: Long, labels: String*): Future[List[Label]] = 32 | api.addLabels(owner, repo, number, labels:_*) 33 | 34 | def replaceLabels(number: Long, labels: String*): Future[List[Label]] = 35 | api.replaceLabels(owner, repo, number, labels:_*) 36 | 37 | def removeAllLabels(number: Long): Future[List[Label]] = 38 | api.removeAllLabels(owner, repo, number) 39 | 40 | def removeLabel(number: Long, label: String): Future[List[Label]] = 41 | api.removeLabel(owner, repo, number, label) 42 | 43 | def listLabels(number: Long): Future[List[Label]] = 44 | api.listLabels(owner, repo, number) 45 | 46 | def listLabelDefs: Future[List[Label]] = 47 | api.listLabelDefs(owner, repo) 48 | 49 | def getLabelDef(label: String): Future[Option[Label]] = 50 | api.getLabelDef(owner, repo, label) 51 | 52 | def createLabelDef(label: LabelInput): Future[Label] = 53 | api.createLabelDef(owner, repo, label) 54 | 55 | def updateLabelDef(name: String, label: LabelInput): Future[Label] = 56 | api.updateLabelDef(owner, repo, name, label) 57 | 58 | def removeLabelDef(label: String): Future[Boolean] = 59 | api.removeLabelDef(owner, repo, label) 60 | 61 | //MilestoneOp 62 | def listMilestones(option: MilestoneListOption = MilestoneListOption()): Future[List[Milestone]] = 63 | api.listMilestones(owner, repo, option) 64 | 65 | def getMilestone(number: Int): Future[Option[Milestone]] = 66 | api.getMilestone(owner, repo, number) 67 | 68 | def createMilestone(input: MilestoneInput): Future[Milestone] = 69 | api.createMilestone(owner, repo, input) 70 | 71 | def updateMilestone(number: Int, input: MilestoneInput): Future[Milestone] = 72 | api.updateMilestone(owner, repo, number, input) 73 | 74 | def removeMilestone(number: Int): Future[Boolean] = 75 | api.removeMilestone(owner, repo, number) 76 | 77 | // PullRequestOp 78 | def listPullRequests(option: PullRequestListOption = PullRequestListOption()): Future[List[PullRequest]] = 79 | api.listPullRequests(owner, repo, option) 80 | 81 | def getPullRequest(number: Long): Future[Option[PullRequest]] = 82 | api.getPullRequest(owner, repo, number) 83 | 84 | def createPullRequest(input: PullRequestInput): Future[PullRequest] = 85 | api.createPullRequest(owner, repo, input) 86 | 87 | def closePullRequest(number: Long): Future[PullRequest] = 88 | api.closePullRequest(owner, repo, number) 89 | 90 | def addReviewRequest(number: Long, reviewers: String*): Future[ReviewRequest] = 91 | api.addReviewRequest(owner, repo, number, reviewers:_*) 92 | 93 | def removeReviewRequest(number: Long, reviewers: String*): Future[Boolean] = 94 | api.removeReviewRequest(owner, repo, number, reviewers:_*) 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/app/Command.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.app 2 | 3 | import codecheck.github.api.GitHubAPI 4 | import codecheck.github.api.RepositoryAPI 5 | import scala.concurrent.Future 6 | import scala.concurrent.ExecutionContext.Implicits.global 7 | import codecheck.github.utils.Json4s 8 | 9 | trait Command { 10 | implicit val formats = Json4s.formats 11 | 12 | val api: GitHubAPI 13 | 14 | def parseRepo(str: String, origin: Option[Repo] = None): Repo = { 15 | val split = str.split("/") 16 | if (split.length == 2) { 17 | Repo(split(0), split(1)) 18 | } else { 19 | Repo(origin.map(_.owner).getOrElse(api.user.login), str) 20 | } 21 | } 22 | 23 | def withRepo(repo: Option[Repo])(f: RepositoryAPI => Future[Any]): Future[Any] = { 24 | repo.map { v => 25 | val rapi = api.repositoryAPI(v.owner, v.name) 26 | f(rapi) 27 | }.getOrElse { 28 | println("Repository not specified") 29 | Future(false) 30 | } 31 | } 32 | 33 | def run(setting: CommandSetting, args: List[String]): Future[CommandSetting] 34 | } -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/app/CommandRunner.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.app 2 | 3 | import scala.concurrent.Future 4 | import scala.concurrent.ExecutionContext.Implicits.global 5 | import codecheck.github.api.GitHubAPI 6 | import codecheck.github.app.commands._ 7 | 8 | class CommandRunner(api: GitHubAPI) { 9 | var commands: Map[String, Command] = Map( 10 | "cr" -> new ChangeRepositoryCommand(api), 11 | "label" -> new LabelCommand(api), 12 | "milestone" -> new MilestoneCommand(api), 13 | "repo" -> new RepositoryCommand(api), 14 | "issue" -> new IssueCommand(api) 15 | ) 16 | 17 | var setting = CommandSetting() 18 | 19 | def split(s: String): List[String] = { 20 | val r = "[^\\s\"']+|\"[^\"]*\"|'[^']*'".r 21 | r.findAllMatchIn(s).toList.map { m => 22 | val str = m.toString 23 | if (str.startsWith("\"") && str.endsWith("\"")) { 24 | str.substring(1, str.length - 1) 25 | } else { 26 | str 27 | } 28 | } 29 | } 30 | def prompt = { 31 | val repo = setting.repo.map(v => "@" + v.owner + "/" + v.name).getOrElse("") 32 | print(api.user.login + repo + ">") 33 | } 34 | 35 | def help = { 36 | println("Avaiable commands:") 37 | commands.keys.toList.sorted.foreach(key => println(s" - $key")) 38 | } 39 | 40 | def process(line: String) = { 41 | try { 42 | val args = split(line) 43 | val ret = args match { 44 | case Nil => 45 | Future(setting) 46 | case "help" :: Nil => 47 | help 48 | Future(setting) 49 | case "help" :: cmd :: Nil => 50 | commands.get(cmd).map(_.run(setting, "help" :: Nil)).getOrElse { 51 | println("Unknown command: " + cmd) 52 | Future(setting) 53 | } 54 | case cmd :: tail => 55 | commands.get(cmd).map(_.run(setting, tail)).getOrElse { 56 | println("Unknown command: " + cmd) 57 | Future(setting) 58 | } 59 | } 60 | ret.map { s => 61 | setting = s 62 | prompt 63 | }.failed.map { e => 64 | println(e.getMessage) 65 | prompt 66 | } 67 | } catch { 68 | case e: Exception => 69 | println(e.toString) 70 | prompt 71 | } 72 | } 73 | 74 | def run = { 75 | prompt 76 | Iterator.continually(Console.in.readLine).takeWhile { s => 77 | val end = s == null || s.trim == "exit" 78 | if (end) { 79 | api.close 80 | println("Bye!") 81 | } 82 | !end 83 | }.foreach { line => 84 | Option(line).map(process(_)) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/app/CommandSetting.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.app 2 | 3 | case class CommandSetting(repo: Option[Repo] = None) { 4 | def repositoryOwner = repo.map(_.owner) 5 | } 6 | 7 | case class Repo(owner: String, name: String) 8 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/app/Main.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.app 2 | 3 | import java.util.Base64 4 | import scala.concurrent.Future 5 | import scala.concurrent.ExecutionContext.Implicits.global 6 | import codecheck.github.api.GitHubAPI 7 | import codecheck.github.exceptions.UnauthorizedException 8 | import codecheck.github.transport.asynchttp19.AsyncHttp19Transport 9 | import com.ning.http.client.AsyncHttpClient 10 | import scopt.OptionParser 11 | 12 | object Main { 13 | 14 | case class Config(user: String = "", pass: String = "") { 15 | def userToken: Option[(String, String)] = { 16 | if (user.length > 0 && pass.length > 0) { 17 | val encoded = Base64.getEncoder.encodeToString((user + ":" + pass).getBytes("utf-8")) 18 | Some((encoded, "Basic")) 19 | } else { 20 | None 21 | } 22 | } 23 | } 24 | 25 | val appName = "gh-shell" 26 | val parser = new OptionParser[Config](appName) { 27 | head(appName, "0.1.0") 28 | opt[String]('u', "user") action { (x, c) => 29 | c.copy(user=x) 30 | } text("username for GitHub") 31 | opt[String]('p', "password") action { (x, c) => 32 | c.copy(pass=x) 33 | } text("password") 34 | note(s""" 35 | |Shell for GitHub 36 | | 37 | |#Use with login 38 | | 39 | | $appName -u USERNAME - p PASSWORD 40 | | 41 | |#Use with GITHUB_TOKEN which set in environment variable 42 | | 43 | | env GITHUB_TOKEN=YOUR_GITHUB_TOKEN $appName 44 | """.stripMargin) 45 | } 46 | 47 | def run(config: Config) = { 48 | config.userToken.orElse { 49 | sys.env.get("GITHUB_TOKEN").map(s => (s, "token")) 50 | }.map { case (token, tokenType) => 51 | val client = new AsyncHttp19Transport(new AsyncHttpClient()) 52 | val api = new GitHubAPI(token, client, tokenType) 53 | try { 54 | api.user 55 | new CommandRunner(api).run 56 | 0 57 | } catch { 58 | case e: UnauthorizedException => 59 | api.close 60 | println("Unauthorized user") 61 | -1 62 | } 63 | }.getOrElse { 64 | parser.showUsage 65 | -1 66 | } 67 | } 68 | 69 | def main(args: Array[String]): Unit = { 70 | parser.parse(args, Config()) match { 71 | case Some(config) => run(config) 72 | case None => 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/app/commands/ChangeRepositoryCommand.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.app.commands 2 | 3 | import scala.concurrent.ExecutionContext.Implicits.global 4 | import codecheck.github.api.GitHubAPI 5 | import scala.concurrent.Future 6 | import codecheck.github.app.Repo 7 | import codecheck.github.app.Command 8 | import codecheck.github.app.CommandSetting 9 | 10 | class ChangeRepositoryCommand(val api: GitHubAPI) extends Command { 11 | 12 | def check(repo: Repo): Future[Some[Repo]] = { 13 | api.getRepository(repo.owner, repo.name).map{ret => 14 | ret.map { v => 15 | val p = v.permissions 16 | print("Your permissions: ") 17 | if (p.admin) print("admin ") 18 | if (p.push) print("push ") 19 | if (p.pull) print("pull ") 20 | println 21 | v 22 | }.orElse { 23 | println(s"Repository ${repo.owner}/${repo.name} is not found.") 24 | None 25 | } 26 | }.transform( 27 | (_ => Some(repo)), 28 | (_ => new Exception(s"Repository ${repo.owner}/${repo.name} is not found.")) 29 | ) 30 | } 31 | 32 | def run(setting: CommandSetting, args: List[String]): Future[CommandSetting] = { 33 | val repo = args match { 34 | case str :: Nil => 35 | var split = str.split("/") 36 | val repo = if (split.length == 2) { 37 | Repo(split(0), split(1)) 38 | } else { 39 | Repo(setting.repositoryOwner.getOrElse(api.user.login), str) 40 | } 41 | Some(repo) 42 | case owner :: repo :: Nil => 43 | Some(Repo(owner, repo)) 44 | case _ => 45 | println("cr [OWNER] [REPO]") 46 | None 47 | } 48 | repo.map(check(_).map(v => setting.copy(repo=v))).getOrElse(Future(setting)) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/app/commands/IssueCommand.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.app.commands 2 | 3 | import java.io.File 4 | import codecheck.github.api.GitHubAPI 5 | import scala.concurrent.Future 6 | import scala.concurrent.ExecutionContext.Implicits.global 7 | import codecheck.github.app.Command 8 | import codecheck.github.app.CommandSetting 9 | import codecheck.github.app.Repo 10 | import codecheck.github.models.IssueListOption 11 | import codecheck.github.models.IssueFilter 12 | import codecheck.github.models.IssueStateFilter 13 | import codecheck.github.models.IssueSort 14 | import codecheck.github.models.SortDirection 15 | import codecheck.github.utils.PrintList 16 | import scopt.OptionParser 17 | import org.joda.time.DateTime 18 | import java.util.Calendar 19 | 20 | class IssueCommand(val api: GitHubAPI) extends Command { 21 | case class Config( 22 | cmd: String = "list", 23 | user: Boolean = false, 24 | org: Option[String] = None, 25 | filter: String = "assigned", 26 | state: String = "open", 27 | labels: Seq[String] = Nil, 28 | sort: String = "created", 29 | direction: String = "desc", 30 | since: Option[Calendar] = None 31 | ) { 32 | 33 | def listOption = IssueListOption( 34 | IssueFilter.fromString(filter), 35 | IssueStateFilter.fromString(state), 36 | labels, 37 | IssueSort.fromString(sort), 38 | SortDirection.fromString(direction), 39 | since.map(new DateTime(_)) 40 | ) 41 | } 42 | 43 | val parser = new OptionParser[Config]("issue") { 44 | cmd("list") action { (x, c) => 45 | c.copy(cmd="list") 46 | } text("List issues") children( 47 | opt[Unit]('u', "user") action { (_, c) => 48 | c.copy(user=true) 49 | }, 50 | opt[String]('o', "org") action { (x, c) => 51 | c.copy(org=Some(x)) 52 | } text("Organization name for listing issues."), 53 | opt[String]('f', "filter") action { (x, c) => 54 | c.copy(filter=x) 55 | } text("Indicates which sorts of issues to return. Can be one of assigned, created, mentioned, subscribed, all. Default: assigned."), 56 | opt[String]("state") action { (x, c) => 57 | c.copy(state=x) 58 | } text("Indicates the state of the issues to return. Can be either open, closed, or all. Default: open"), 59 | opt[Seq[String]]('l', "labels") action { (x, c) => 60 | c.copy(labels=x) 61 | } text("A list of comma separated label names. Example: bug,ui,@high"), 62 | opt[String]("sort") action { (x, c) => 63 | c.copy(sort=x) 64 | } text("What to sort results by. Can be either created, updated, comments. Default: created"), 65 | opt[String]("direction") action { (x, c) => 66 | c.copy(direction=x) 67 | } text("The direction of the sort. Either asc or desc. Default: asc"), 68 | opt[Calendar]("since") action { (x, c) => 69 | c.copy(since=Some(x)) 70 | } text("Only issues updated at or after this time are returned. This is a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ.") 71 | ) 72 | 73 | } 74 | 75 | def run(setting: CommandSetting, args: List[String]): Future[CommandSetting] = { 76 | parser.parse(args, new Config()) match { 77 | case Some(config) => 78 | runSubcommand(config).map(_ => setting) 79 | case None => 80 | Future(setting) 81 | } 82 | } 83 | 84 | def runSubcommand(config: Config): Future[Any] = { 85 | config.cmd match { 86 | case "list" => 87 | list(config) 88 | case _ => 89 | parser.showUsage 90 | Future(true) 91 | } 92 | } 93 | 94 | def list(config: Config): Future[Any] = { 95 | val option = config.listOption 96 | val future = if (config.user) { 97 | api.listUserIssues(option) 98 | } else { 99 | config.org.map { v => 100 | api.listOrgIssues(v, option) 101 | }.getOrElse { 102 | api.listAllIssues(option) 103 | } 104 | } 105 | future.map { 106 | _.groupBy(i => i.repository.owner.login + "/" + i.repository.name) 107 | .map { case (name, list) => 108 | println 109 | println(s"*** $name") 110 | val rows = list.map { i => 111 | List( 112 | i.number, 113 | i.title, 114 | i.assignee.map(_.login).getOrElse(""), 115 | i.milestone.map(_.title).getOrElse(""), 116 | i.comments, 117 | i.labels.map(_.name).mkString(", ") 118 | ) 119 | } 120 | PrintList("No.", "title", "assignee", "milestone", "comments", "labels").build(rows) 121 | } 122 | } 123 | } 124 | 125 | } 126 | 127 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/app/commands/LabelCommand.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.app.commands 2 | 3 | import java.io.File 4 | import codecheck.github.api.GitHubAPI 5 | import scala.concurrent.Future 6 | import scala.concurrent.ExecutionContext.Implicits.global 7 | import codecheck.github.app.Command 8 | import codecheck.github.app.CommandSetting 9 | import codecheck.github.app.Repo 10 | import codecheck.github.models.Label 11 | import codecheck.github.models.LabelInput 12 | import codecheck.github.utils.PrintList 13 | import scopt.OptionParser 14 | import org.json4s._ 15 | import org.json4s.jackson.JsonMethods 16 | 17 | 18 | class LabelCommand(val api: GitHubAPI) extends Command { 19 | case class Config( 20 | repo: Option[Repo], 21 | cmd: String = "list", 22 | name: String = "", 23 | color: String = "", 24 | file: File = null, 25 | newName: Option[String] = None 26 | ) 27 | 28 | val parser = new OptionParser[Config]("label") { 29 | opt[String]('r', "repo") action { (x, c) => 30 | c.copy(repo=Some(parseRepo(x, c.repo))) 31 | } text("target repository [OWNER]/[REPO]") 32 | 33 | cmd("list") action { (x, c) => 34 | c.copy(cmd="list") 35 | } text("list labels") 36 | 37 | cmd("add") action { (x, c) => 38 | c.copy(cmd="add") 39 | } text("add a label") children ( 40 | arg[String]("") action { (x, c) => 41 | c.copy(name=x) 42 | } text("Label name"), 43 | 44 | arg[String]("") action { (x, c) => 45 | c.copy(color=x) 46 | } text("Label color") 47 | ) 48 | 49 | cmd("merge") action { (x, c) => 50 | c.copy(cmd="merge") 51 | } text("add or update lables from file") children( 52 | arg[File]("") action { (x, c) => 53 | c.copy(file=x) 54 | } text("Json file to merge") 55 | ) 56 | 57 | cmd("update") action { (x, c) => 58 | c.copy(cmd="update") 59 | } text("update a label") children ( 60 | arg[String]("") action { (x, c) => 61 | c.copy(name=x) 62 | } text("Label name"), 63 | 64 | arg[String]("") action { (x, c) => 65 | c.copy(color=x) 66 | } text("Label color"), 67 | 68 | opt[String]('n', "newname") action { (x, c) => 69 | c.copy(newName=Some(x)) 70 | } text("New name for label") 71 | ) 72 | 73 | cmd("rm") action { (x, c) => 74 | c.copy(cmd="rm") 75 | } text("remove a label") children ( 76 | arg[String]("") action { (x, c) => 77 | c.copy(name=x) 78 | } text("Label name") 79 | ) 80 | 81 | } 82 | 83 | def run(setting: CommandSetting, args: List[String]): Future[CommandSetting] = { 84 | parser.parse(args, new Config(setting.repo)) match { 85 | case Some(config) => 86 | runSubcommand(config).map(_ => setting) 87 | case None => 88 | Future(setting) 89 | } 90 | } 91 | 92 | def runSubcommand(config: Config): Future[Any] = { 93 | config.cmd match { 94 | case "list" => 95 | list(config) 96 | case "add" => 97 | add(config) 98 | case "merge" => 99 | merge(config) 100 | case "update" => 101 | update(config) 102 | case "rm" => 103 | remove(config) 104 | case _ => 105 | parser.showUsage 106 | Future(true) 107 | } 108 | } 109 | 110 | def list(config: Config): Future[Any] = withRepo(config.repo) { rapi => 111 | rapi.listLabelDefs.map { list => 112 | val rows = list.map { label => 113 | List(label.name, label.color) 114 | } 115 | PrintList("label", "color").build(rows) 116 | true 117 | } 118 | } 119 | 120 | def add(config: Config): Future[Any] = withRepo(config.repo) { rapi => 121 | val input = LabelInput(config.name, config.color) 122 | rapi.createLabelDef(input).map { l => 123 | println(s"Created ${l.name} - ${l.color}") 124 | true 125 | } 126 | } 127 | 128 | def update(config: Config): Future[Any] = withRepo(config.repo) { rapi => 129 | val input = LabelInput(config.newName.getOrElse(config.name), config.color) 130 | rapi.updateLabelDef(config.name, input).map { l => 131 | println(s"Updated ${l.name} - ${l.color}") 132 | true 133 | } 134 | } 135 | 136 | def remove(config: Config): Future[Any] = withRepo(config.repo) { rapi => 137 | rapi.removeLabelDef(config.name).map { b => 138 | println(s"Removed ${config.name}") 139 | true 140 | } 141 | } 142 | 143 | def merge(config: Config): Future[Any] = withRepo(config.repo) { rapi => 144 | def doCreateLabel(label: Option[Label], input: LabelInput): Future[String] = { 145 | label match { 146 | case Some(l) if (l.color == input.color) => 147 | Future(s"Skip create label ${input.name}") 148 | case Some(l) => 149 | rapi.updateLabelDef(input.name, input).map(_ => s"Update label ${input.name}") 150 | case None => 151 | rapi.createLabelDef(input).map(_ => s"Create label ${input.name}") 152 | } 153 | } 154 | val json = JsonMethods.parse(config.file) 155 | val items = (json match { 156 | case JArray(list) => list 157 | case JObject => List(json) 158 | case _ => throw new IllegalArgumentException("Invalid json file\n" + JsonMethods.pretty(json)) 159 | }).map(v => LabelInput( 160 | (v \ "name").extract[String], 161 | (v \ "color").extract[String] 162 | )) 163 | rapi.listLabelDefs.flatMap { labels => 164 | val ret = items.map { input => 165 | doCreateLabel(labels.find(_.name == input.name), input).map { s => 166 | println(s) 167 | s 168 | } 169 | } 170 | Future.sequence(ret) 171 | } 172 | } 173 | 174 | } 175 | 176 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/app/commands/MilestoneCommand.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.app.commands 2 | 3 | import java.io.File 4 | import java.util.Calendar 5 | import org.joda.time.DateTime 6 | import codecheck.github.api.GitHubAPI 7 | import scala.concurrent.Future 8 | import scala.concurrent.ExecutionContext.Implicits.global 9 | import codecheck.github.app.Command 10 | import codecheck.github.app.CommandSetting 11 | import codecheck.github.app.Repo 12 | import codecheck.github.models.AbstractJson 13 | import codecheck.github.models.Milestone 14 | import codecheck.github.models.MilestoneInput 15 | import codecheck.github.models.MilestoneListOption 16 | import codecheck.github.models.MilestoneState 17 | import codecheck.github.models.MilestoneSort 18 | import codecheck.github.models.SortDirection 19 | import codecheck.github.utils.PrintList 20 | import scopt.OptionParser 21 | import org.json4s._ 22 | import org.json4s.jackson.JsonMethods 23 | 24 | 25 | class MilestoneCommand(val api: GitHubAPI) extends Command { 26 | case class Config( 27 | repo: Option[Repo], 28 | cmd: String = "list", 29 | sort: String = "due_date", 30 | direction: String = "asc", 31 | verbose: Boolean = false, 32 | title: Option[String] = None, 33 | description: Option[String] = None, 34 | due_on: Option[Calendar] = None, 35 | state: Option[String] = None, 36 | number: Int = 0, 37 | file: File = null 38 | ) { 39 | def input = MilestoneInput( 40 | title, 41 | state.map(MilestoneState.fromString(_)), 42 | description, 43 | due_on.map(new DateTime(_)) 44 | ) 45 | 46 | def listOption = MilestoneListOption( 47 | MilestoneState.fromString(state.getOrElse("open")), 48 | MilestoneSort.fromString(sort), 49 | SortDirection.fromString(direction) 50 | ) 51 | } 52 | 53 | val parser = new OptionParser[Config]("milestone") { 54 | opt[String]('r', "repo") action { (x, c) => 55 | c.copy(repo=Some(parseRepo(x, c.repo))) 56 | } text("target repository [OWNER]/[REPO]") 57 | 58 | cmd("list") action { (x, c) => 59 | c.copy(cmd="list") 60 | } text("List milestones") children( 61 | opt[String]("state") action { (x, c) => 62 | c.copy(state=Some(x)) 63 | } text("The state of the milestone. Either open, closed, or all. Default: open"), 64 | opt[String]("sort") action { (x, c) => 65 | c.copy(sort=x) 66 | } text("What to sort results by. Either due_date or completeness. Default: due_date"), 67 | opt[String]("direction") action { (x, c) => 68 | c.copy(direction=x) 69 | } text("The direction of the sort. Either asc or desc. Default: asc"), 70 | opt[Unit]('v', "verbose") action { (_, c) => 71 | c.copy(verbose=true) 72 | } text("Show detailed output or not. Default: false") 73 | ) 74 | 75 | cmd("add") action { (x, c) => 76 | c.copy(cmd="add") 77 | } text("Create a milestone.") children ( 78 | arg[String]("") action { (x, c) => 79 | c.copy(title=Some(x)) 80 | } text("Required. The title of the milestone."), 81 | 82 | opt[String]('D', "description") action { (x, c) => 83 | c.copy(description=Some(x)) 84 | } text("A description of the milestone."), 85 | 86 | opt[Calendar]('d', "due_on") action { (x, c) => 87 | c.copy(due_on=Some(x)) 88 | } text("The milestone due date. Format: YYYY-MM-DD.") 89 | ) 90 | 91 | cmd("merge") action { (x, c) => 92 | c.copy(cmd="merge") 93 | } text("Create or update milestones from file") children( 94 | arg[File]("<file>") action { (x, c) => 95 | c.copy(file=x) 96 | } text("Json file to merge") 97 | ) 98 | 99 | cmd("update") action { (x, c) => 100 | c.copy(cmd="update") 101 | } text("Update a milestone.") children ( 102 | arg[Int]("<number>") action { (x, c) => 103 | c.copy(number=x) 104 | } text("Required. The number of milestone."), 105 | 106 | opt[String]('t', "title") action { (x, c) => 107 | c.copy(title=Some(x)) 108 | } text("The title of the milestone."), 109 | 110 | opt[String]('s', "state") action { (x, c) => 111 | c.copy(state=Some(x)) 112 | } text("The state of the milestone. Either open or closed."), 113 | 114 | opt[Unit]('c', "close") action { (_, c) => 115 | c.copy(state=Some("closed")) 116 | } text("Change state to closed."), 117 | 118 | opt[String]('D', "description") action { (x, c) => 119 | c.copy(description=Some(x)) 120 | } text("A description of the milestone."), 121 | 122 | opt[Calendar]('d', "due_on") action { (x, c) => 123 | c.copy(due_on=Some(x)) 124 | } text("The milestone due date. Format: YYYY-MM-DD.") 125 | ) 126 | 127 | cmd("rm") action { (x, c) => 128 | c.copy(cmd="rm") 129 | } text("remove a label") children ( 130 | arg[Int]("<number>") action { (x, c) => 131 | c.copy(number=x) 132 | } text("Required. The number of milestone.") 133 | ) 134 | 135 | } 136 | 137 | def run(setting: CommandSetting, args: List[String]): Future[CommandSetting] = { 138 | parser.parse(args, new Config(setting.repo)) match { 139 | case Some(config) => 140 | runSubcommand(config).map(_ => setting) 141 | case None => 142 | Future(setting) 143 | } 144 | } 145 | 146 | def runSubcommand(config: Config): Future[Any] = { 147 | config.cmd match { 148 | case "list" => 149 | list(config) 150 | case "add" => 151 | add(config) 152 | case "merge" => 153 | merge(config) 154 | case "update" => 155 | update(config) 156 | case "rm" => 157 | remove(config) 158 | case _ => 159 | parser.showUsage 160 | Future(true) 161 | } 162 | } 163 | 164 | def printDetail(m: Milestone): Unit = { 165 | println(s""" 166 | |--------------------------------------------- 167 | |${m.title} 168 | | number: ${m.number} 169 | | due_on: ${m.due_on.getOrElse("")} 170 | | open : ${m.open_issues} 171 | | closed: ${m.closed_issues} 172 | """.stripMargin) 173 | m.description.foreach(println(_)) 174 | } 175 | 176 | def list(config: Config): Future[Any] = withRepo(config.repo) { rapi => 177 | rapi.listMilestones(config.listOption).map { list => 178 | if (config.verbose) { 179 | list.foreach(printDetail) 180 | } else { 181 | val rows = list.map { m => 182 | List( 183 | m.number, 184 | m.title, 185 | s"${m.open_issues}/${m.open_issues + m.closed_issues}", 186 | m.due_on.map(_.toString("yyyy-MM-dd")).getOrElse("") 187 | ) 188 | } 189 | PrintList("No.", "title", "count", "due_on").build(rows) 190 | } 191 | true 192 | } 193 | } 194 | 195 | def add(config: Config): Future[Any] = withRepo(config.repo) { rapi => 196 | val input = config.input 197 | rapi.createMilestone(input).map { m => 198 | printDetail(m) 199 | true 200 | } 201 | } 202 | 203 | def update(config: Config): Future[Any] = withRepo(config.repo) { rapi => 204 | val input = config.input 205 | rapi.updateMilestone(config.number, input).map { m => 206 | printDetail(m) 207 | true 208 | } 209 | } 210 | 211 | def remove(config: Config): Future[Any] = withRepo(config.repo) { rapi => 212 | rapi.removeMilestone(config.number).map { b => 213 | println(s"Removed ${config.number}") 214 | true 215 | } 216 | } 217 | 218 | def merge(config: Config): Future[Any] = withRepo(config.repo) { rapi => 219 | def doCreateMilestone(milestone: Option[Milestone], input: MilestoneInput): Future[String] = { 220 | milestone match { 221 | case Some(m) => 222 | rapi.updateMilestone(m.number, input).map(_ => s"Update milestone ${m.title}") 223 | case None => 224 | rapi.createMilestone(input).map(m => s"Create milestone ${m.title}") 225 | } 226 | } 227 | val json = JsonMethods.parse(config.file) 228 | val items = (json match { 229 | case JArray(list) => list 230 | case JObject => List(json) 231 | case _ => throw new IllegalArgumentException("Invalid json file\n" + JsonMethods.pretty(json)) 232 | }).map{ v => 233 | val json = new AbstractJson(v) 234 | MilestoneInput( 235 | Some(json.get("title")), 236 | json.opt("state").map(MilestoneState.fromString(_)), 237 | json.opt("description"), 238 | json.opt("due_on").map(new DateTime(_)) 239 | ) 240 | } 241 | rapi.listMilestones(MilestoneListOption(state=MilestoneState.all)).flatMap { milestones => 242 | val ret = items.map { input => 243 | doCreateMilestone(milestones.find( 244 | m => input.title.exists(_ == m.title) 245 | ), input).map { s => 246 | println(s) 247 | s 248 | } 249 | } 250 | Future.sequence(ret) 251 | } 252 | } 253 | 254 | } 255 | 256 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/app/commands/RepositoryCommand.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.app.commands 2 | 3 | import java.io.File 4 | import codecheck.github.api.GitHubAPI 5 | import scala.concurrent.Future 6 | import scala.concurrent.ExecutionContext.Implicits.global 7 | import codecheck.github.app.Command 8 | import codecheck.github.app.CommandSetting 9 | import codecheck.github.app.Repo 10 | import codecheck.github.models.Repository 11 | import codecheck.github.models.RepositoryListOption 12 | import codecheck.github.models.RepositoryListType 13 | import codecheck.github.models.RepositorySort 14 | import codecheck.github.models.SortDirection 15 | import codecheck.github.utils.PrintList 16 | import scopt.OptionParser 17 | 18 | 19 | class RepositoryCommand(val api: GitHubAPI) extends Command { 20 | case class Config( 21 | cmd: String = "list", 22 | user: Option[String] = None, 23 | org: Option[String] = None, 24 | listType: String = "all", 25 | sort: String = "full_name", 26 | direction: String = "asc" 27 | ) 28 | 29 | val parser = new OptionParser[Config]("label") { 30 | cmd("list") action { (x, c) => 31 | c.copy(cmd="list") 32 | } text("list repositories") children( 33 | opt[String]('u', "user") action { (x, c) => 34 | c.copy(user=Some(x)) 35 | } text("List public repositories for the specified user."), 36 | opt[String]('o', "org") action { (x, c) => 37 | c.copy(org=Some(x)) 38 | } text("List repositories for the specified org."), 39 | opt[String]("type") action { (x, c) => 40 | c.copy(listType=x) 41 | } text("Can be one of all, owner, public, private, member, forks, sources. Default: all"), 42 | opt[String]("sort") action { (x, c) => 43 | c.copy(sort=x) 44 | } text("Can be one of created, updated, pushed, full_name. Default: full_name"), 45 | opt[String]("direction") action { (x, c) => 46 | c.copy(direction=x) 47 | } text("The direction of the sort. Either asc or desc. Default: asc") 48 | ) 49 | 50 | } 51 | 52 | def run(setting: CommandSetting, args: List[String]): Future[CommandSetting] = { 53 | parser.parse(args, new Config()) match { 54 | case Some(config) => 55 | runSubcommand(config).map(_ => setting) 56 | case None => 57 | Future(setting) 58 | } 59 | } 60 | 61 | def runSubcommand(config: Config): Future[Any] = { 62 | config.cmd match { 63 | case "list" => 64 | list(config) 65 | case _ => 66 | parser.showUsage 67 | Future(true) 68 | } 69 | } 70 | 71 | def list(config: Config): Future[Any] = { 72 | val option = RepositoryListOption( 73 | RepositoryListType.fromString(config.listType), 74 | RepositorySort.fromString(config.sort), 75 | SortDirection.fromString(config.direction) 76 | ) 77 | config.user.map(u => api.listUserRepositories(u, option)) 78 | .orElse(config.org.map(o => api.listOrgRepositories(o, option))) 79 | .getOrElse(api.listOwnRepositories()) 80 | .map { list => 81 | val owner = config.user.orElse(config.org).getOrElse(api.user.login) 82 | val rows = list.map { repo => 83 | List(repo.name, repo.description.getOrElse(""), repo.open_issues_count) 84 | } 85 | PrintList("name", "description", "issues").build(rows) 86 | true 87 | } 88 | } 89 | 90 | } 91 | 92 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/events/DefaultEvent.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.events 2 | 3 | import org.json4s.JValue 4 | import codecheck.github.models.AbstractJson 5 | 6 | case class DefaultEvent(name: String, value: JValue) extends AbstractJson(value) with GitHubEvent 7 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/events/GitHubEvent.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.events 2 | 3 | import org.json4s.JValue 4 | import org.json4s.jackson.JsonMethods 5 | import codecheck.github.models.AbstractJson 6 | 7 | trait GitHubEvent { 8 | val name: String 9 | val value: JValue 10 | 11 | lazy val repositoryName = new AbstractJson(value \ "repository").get("name") 12 | lazy val ownerName = { 13 | val user = new AbstractJson(value \ "repository" \ "owner") 14 | user.opt("login").getOrElse(user.get("name")) 15 | } 16 | override def toString = name + "\n" + JsonMethods.pretty(value) 17 | } 18 | 19 | object GitHubEvent { 20 | def apply(name: String, value: JValue): GitHubEvent = name match { 21 | case "issues" => IssueEvent(name, value) 22 | case "issue_comment" => IssueCommentEvent(name, value) 23 | case "pull_request" => PullRequestEvent(name, value) 24 | case "pull_request_review" => PullRequestReviewEvent(name, value) 25 | case "push" => PushEvent(name, value) 26 | case _ => DefaultEvent(name, value) 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/events/IssueCommentEvent.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.events 2 | 3 | import org.json4s.JValue 4 | import codecheck.github.models.Issue 5 | import codecheck.github.models.Comment 6 | import codecheck.github.models.AbstractJson 7 | 8 | case class IssueCommentEvent(name: String, value: JValue) extends AbstractJson(value) with GitHubEvent { 9 | lazy val issue = Issue(value \ "issue") 10 | lazy val comment = Comment(value \ "comment") 11 | } 12 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/events/IssueEvent.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.events 2 | 3 | import org.json4s.JValue 4 | import codecheck.github.models.AbstractJson 5 | import codecheck.github.models.Issue 6 | import codecheck.github.models.IssueAction 7 | import codecheck.github.models.Comment 8 | 9 | case class IssueEvent(name: String, value: JValue) extends AbstractJson(value) with GitHubEvent { 10 | 11 | lazy val action = IssueAction.fromString(get("action")) 12 | lazy val issue = Issue(value \ "issue") 13 | } 14 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/events/PullRequestEvent.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.events 2 | 3 | import org.json4s.JValue 4 | import codecheck.github.models.AbstractJson 5 | import codecheck.github.models.PullRequest 6 | import codecheck.github.models.PullRequestAction 7 | 8 | case class PullRequestEvent(name: String, value: JValue) extends AbstractJson(value) with GitHubEvent { 9 | def number = get("number").toLong 10 | 11 | lazy val action = PullRequestAction.fromString(get("action")) 12 | lazy val pull_request = PullRequest(value \ "pull_request") 13 | } 14 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/events/PullRequestReviewEvent.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github 2 | package events 3 | 4 | import org.json4s.JValue 5 | import codecheck.github.models.AbstractJson 6 | import codecheck.github.models.PullRequest 7 | import codecheck.github.models.PullRequestReview 8 | import codecheck.github.models.PullRequestReviewAction 9 | import codecheck.github.models.Repository 10 | import codecheck.github.models.User 11 | 12 | case class PullRequestReviewEvent(name: String, value: JValue) extends AbstractJson(value) with GitHubEvent { 13 | lazy val action = PullRequestReviewAction.fromString(get("action")) 14 | lazy val review = PullRequestReview(value \ "review") 15 | lazy val pull_request = models.PullRequest(value \ "pull_request") 16 | lazy val repository = new Repository(value \ "repository") 17 | lazy val sender = new User(value \ "sender") 18 | } 19 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/events/PushEvent.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.events 2 | 3 | import org.json4s.JValue 4 | import org.json4s.JArray 5 | import codecheck.github.models.AbstractJson 6 | import codecheck.github.models.PullRequest 7 | import codecheck.github.models.PullRequestAction 8 | import codecheck.github.models.Repository 9 | import codecheck.github.models.User 10 | 11 | case class PushCommit(value: JValue) extends AbstractJson(value) { 12 | def id = get("id") 13 | def url = get("url") 14 | def tree_id = get("tree_id") 15 | def distinct = boolean("distinct") 16 | def message = get("message") 17 | def timestamp = get("timestamp") 18 | lazy val author = User(value \ "author") 19 | lazy val committer = User(value \ "committer") 20 | def added = seq("added") 21 | def removed = seq("removed") 22 | def modified = seq("modified") 23 | } 24 | 25 | case class PushEvent(name: String, value: JValue) extends AbstractJson(value) with GitHubEvent { 26 | def ref = get("ref") 27 | def before = get("before") 28 | def after = get("after") 29 | lazy val base_ref = opt("base_ref") 30 | lazy val head_commit = PushCommit(value \ "head_commit") 31 | lazy val commits = (value \ "commits") match { 32 | case JArray(arr) => arr.map(PushCommit(_)) 33 | case _ => Seq.empty[PushCommit] 34 | } 35 | lazy val repository = Repository(value \ "repository") 36 | lazy val pusher = User(value \ "pusher") 37 | lazy val sender = User(value \ "sender") 38 | } 39 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/exceptions/GitHubAPIException.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.exceptions 2 | 3 | import org.json4s.JValue 4 | import codecheck.github.models.ErrorResponse 5 | 6 | class GitHubAPIException(body: JValue) extends Exception { 7 | lazy val error = ErrorResponse(body) 8 | 9 | override def getMessage = error.toString 10 | 11 | } -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/exceptions/NotFoundException.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.exceptions 2 | 3 | import org.json4s.JValue 4 | 5 | class NotFoundException(body: JValue) extends GitHubAPIException(body) 6 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/exceptions/OAuthAPIException.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.exceptions 2 | 3 | import org.json4s.JValue 4 | import codecheck.github.models.OAuthErrorResponse 5 | 6 | class OAuthAPIException(body: JValue) extends Exception { 7 | lazy val error = OAuthErrorResponse(body) 8 | 9 | override def getMessage = error.toString 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/exceptions/PermissionDeniedException.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.exceptions 2 | 3 | import org.json4s.JValue 4 | 5 | class PermissionDeniedException(body: JValue) extends GitHubAPIException(body) 6 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/exceptions/UnauthorizedException.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.exceptions 2 | 3 | import org.json4s.JValue 4 | 5 | class UnauthorizedException(body: JValue) extends GitHubAPIException(body) 6 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/models/AbstractInput.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.models 2 | 3 | import org.json4s._ 4 | import org.json4s.jackson.JsonMethods 5 | import codecheck.github.utils.Json4s.formats 6 | 7 | trait AbstractInput { 8 | val value: JValue = Extraction.decompose(this) 9 | override def toString = JsonMethods.pretty(value) 10 | } 11 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/models/AbstractJson.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.models 2 | 3 | import org.json4s.JValue 4 | import org.json4s.JNothing 5 | import org.json4s.JNull 6 | import org.json4s.JObject 7 | import org.json4s.JArray 8 | import org.json4s.Formats 9 | import org.json4s.DefaultFormats 10 | import org.json4s.jackson.JsonMethods 11 | import codecheck.github.utils.Json4s.formats 12 | 13 | import org.joda.time.DateTime 14 | 15 | class AbstractJson(value: JValue) { 16 | 17 | def opt(path: String): Option[String] = { 18 | path.split("\\.").foldLeft(value) { (v, s) => 19 | v \ s 20 | } match { 21 | case JNothing => None 22 | case JNull => None 23 | case v: JValue => Some(v.extract[String]) 24 | } 25 | } 26 | 27 | def get(path: String) = opt(path).getOrElse("") 28 | 29 | def dateOpt(path: String): Option[DateTime] = { 30 | path.split("\\.").foldLeft(value) { (v, s) => 31 | v \ s 32 | } match { 33 | case JNothing => None 34 | case JNull => None 35 | case v: JValue => Some(v.extract[DateTime]) 36 | } 37 | } 38 | 39 | def getDate(path: String): DateTime = dateOpt(path).get 40 | 41 | def booleanOpt(path: String): Option[Boolean] = { 42 | path.split("\\.").foldLeft(value) { (v, s) => 43 | v \ s 44 | } match { 45 | case JNothing => None 46 | case JNull => None 47 | case v: JValue => Some(v.extract[Boolean]) 48 | } 49 | } 50 | 51 | def boolean(path: String): Boolean = booleanOpt(path).get 52 | 53 | def objectOpt[T](path: String)(f: JValue => T): Option[T] = { 54 | path.split("\\.").foldLeft(value) { (v, s) => 55 | v \ s 56 | } match { 57 | case x: JObject => Some(f(x)) 58 | case _ => None 59 | } 60 | } 61 | 62 | override def toString = JsonMethods.pretty(value) 63 | 64 | def seqOpt[T](path: String): Seq[T] = { 65 | path.split("\\.").foldLeft(value) { (v, s) => 66 | v \ s 67 | } match { 68 | case JNothing => Nil 69 | case JNull => Nil 70 | case v: JArray => v.values.map(_.asInstanceOf[T]) 71 | case v: JValue => List(v.asInstanceOf[T]) 72 | } 73 | } 74 | 75 | def seq(path: String): Seq[String] = seqOpt(path) 76 | } 77 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/models/AccessToken.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.models 2 | 3 | import org.json4s.JValue 4 | 5 | case class AccessToken(value: JValue) extends AbstractJson(value) { 6 | def access_token = get("access_token") 7 | def token_type = get("token_type") 8 | def scope: List[String] = get("scope").split(",").toList 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/models/Branch.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.models 2 | import org.json4s.JValue 3 | 4 | case class BranchListItem(value: JValue) extends AbstractJson(value) { 5 | def name = get("name") 6 | lazy val commit = CommitInfo(value \ "commit") 7 | } 8 | 9 | case class CommitInfo(value: JValue) extends AbstractJson(value) { 10 | def sha = get("sha") 11 | def url = get("url") 12 | } 13 | 14 | case class Branch(value: JValue) extends AbstractJson(value) { 15 | def name = get("name") 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/models/Collaborator.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.models 2 | import org.json4s.JValue 3 | 4 | case class Collaborator(value: JValue) extends AbstractJson(value) { 5 | def login = get("login") 6 | def id = get("id").toLong 7 | def avatar_url = get("avatar_url") 8 | def url = get("url") 9 | def site_admin: Boolean = boolean("site_admin") 10 | } 11 | //case class CollaboratorInput extends AbstractInput 12 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/models/Comment.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.models 2 | 3 | import org.json4s.JValue 4 | 5 | case class Comment(value: JValue) extends AbstractJson(value) { 6 | def body = get("body") 7 | lazy val user = new User(value \ "user") 8 | } 9 | 10 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/models/ErrorResponse.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.models 2 | 3 | import org.json4s.JValue 4 | import org.json4s.JArray 5 | import org.json4s.JNothing 6 | import org.json4s.jackson.JsonMethods 7 | 8 | case class ErrorResponse(value: JValue) extends AbstractJson(value) { 9 | def message = get("message") 10 | lazy val errors: List[ErrorObject] = (value \ "errors") match { 11 | case JArray(ar) => ar.map(e => ErrorObject(e)) 12 | case JNothing => Nil 13 | case _ => throw new IllegalStateException() 14 | } 15 | def documentation_url = opt("documentation_url") 16 | } 17 | 18 | case class ErrorObject(value: JValue) extends AbstractJson(value) { 19 | def resource = get("resource") 20 | def field = get("field") 21 | def code = get("code") 22 | } -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/models/Issue.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.models 2 | 3 | import org.json4s.JValue 4 | import org.json4s.JString 5 | import org.json4s.JNothing 6 | import org.json4s.JNull 7 | import org.json4s.JInt 8 | import org.json4s.JArray 9 | import org.json4s.JsonDSL._ 10 | import org.joda.time.DateTime 11 | import org.joda.time.DateTimeZone 12 | 13 | import codecheck.github.utils.ToDo 14 | import scala.language.implicitConversions 15 | 16 | sealed abstract class IssueState(val name: String) { 17 | override def toString = name 18 | } 19 | 20 | object IssueState { 21 | case object open extends IssueState("open") 22 | case object closed extends IssueState("closed") 23 | 24 | def all = IssueStateFilter.all 25 | 26 | implicit def toIssueStateFilter(state: IssueState) = state match { 27 | case IssueState.open => IssueStateFilter.open 28 | case IssueState.closed => IssueStateFilter.closed 29 | } 30 | 31 | val values = Array(open, closed) 32 | 33 | def fromString(str: String) = values.filter(_.name == str).head 34 | } 35 | 36 | sealed abstract class IssueStateFilter(val name: String) { 37 | override def toString = name 38 | } 39 | 40 | object IssueStateFilter { 41 | case object open extends IssueStateFilter("open") 42 | case object closed extends IssueStateFilter("closed") 43 | case object all extends IssueStateFilter("all") 44 | 45 | val values = Array(open, closed, all) 46 | 47 | def fromString(str: String) = values.filter(_.name == str).head 48 | } 49 | 50 | sealed abstract class IssueFilter(val name: String) { 51 | override def toString = name 52 | } 53 | 54 | object IssueFilter { 55 | case object assigned extends IssueFilter("assigned") 56 | case object created extends IssueFilter("created") 57 | case object mentioned extends IssueFilter("mentioned") 58 | case object subscribed extends IssueFilter("subscribed") 59 | case object all extends IssueFilter("all") 60 | 61 | val values = Array(assigned, created, mentioned, subscribed, all) 62 | 63 | def fromString(str: String) = values.filter(_.name == str).head 64 | } 65 | 66 | sealed abstract class IssueSort(val name: String) { 67 | override def toString = name 68 | } 69 | 70 | object IssueSort { 71 | case object created extends IssueSort("created") 72 | case object updated extends IssueSort("updated") 73 | case object comments extends IssueSort("comments") 74 | 75 | val values = Array(created, updated, comments) 76 | 77 | def fromString(str: String) = values.filter(_.name == str).head 78 | } 79 | 80 | sealed abstract class MilestoneSearchOption(val name: String) { 81 | override def toString = name 82 | } 83 | 84 | object MilestoneSearchOption { 85 | case object all extends MilestoneSearchOption("*") 86 | case object none extends MilestoneSearchOption("none") 87 | case class Specified(number: Int) extends MilestoneSearchOption(number.toString()) 88 | 89 | def apply(number: Int) = Specified(number) 90 | } 91 | 92 | case class IssueListOption( 93 | filter: IssueFilter = IssueFilter.assigned, 94 | state: IssueStateFilter = IssueStateFilter.open, 95 | labels: Seq[String] = Nil, 96 | sort: IssueSort = IssueSort.created, 97 | direction: SortDirection = SortDirection.desc, 98 | since: Option[DateTime] = None 99 | ) { 100 | def q = s"?filter=$filter&state=$state&sort=$sort&direction=$direction" + 101 | (if (!labels.isEmpty) "&labels=" + labels.mkString(",") else "") + 102 | since.map("&since=" + _.toDateTime(DateTimeZone.UTC).toString("yyyy-MM-dd'T'HH:mm:ss'Z'")).getOrElse("") 103 | } 104 | 105 | case class IssueListOption4Repository( 106 | milestone: Option[MilestoneSearchOption] = None, 107 | state: IssueStateFilter = IssueStateFilter.open, 108 | assignee: Option[String] = None, 109 | creator: Option[String] = None, 110 | mentioned: Option[String] = None, 111 | labels: Seq[String] = Nil, 112 | sort: IssueSort = IssueSort.created, 113 | direction: SortDirection = SortDirection.desc, 114 | since: Option[DateTime] = None 115 | ) { 116 | def q = s"?state=$state&sort=$sort&direction=$direction" + 117 | milestone.map(t => s"milestone=$t&").getOrElse("") + 118 | assignee.map(t => s"&assignee=$t").getOrElse("") + 119 | creator.map(t => s"&creator=$t").getOrElse("") + 120 | mentioned.map(t => s"&mentioned=$t").getOrElse("") + 121 | (if (!labels.isEmpty) "&labels=" + labels.mkString(",") else "") + 122 | since.map("&since=" + _.toDateTime(DateTimeZone.UTC).toString("yyyy-MM-dd'T'HH:mm:ss'Z'")).getOrElse("") 123 | } 124 | 125 | case class IssueInput( 126 | title: Option[String] = None, 127 | body: Option[String] = None, 128 | assignee: Option[String] = None, 129 | milestone: Option[Int] = None, 130 | labels: Seq[String] = Nil, 131 | state: Option[IssueStateFilter] = None 132 | ) extends AbstractInput { 133 | override val value: JValue = { 134 | val a = assignee.map { s => 135 | if (s.length == 0) JNull else JString(s) 136 | }.getOrElse(JNothing) 137 | val l = if (labels.length == 0) JNothing else JArray(labels.map(JString(_)).toList) 138 | 139 | ("title" -> title) ~ 140 | ("body" -> body) ~ 141 | ("assignee" -> a) ~ 142 | ("milestone" -> milestone) ~ 143 | ("labels" -> l) ~ 144 | ("state" -> state.map(_.name)) 145 | } 146 | } 147 | 148 | object IssueInput { 149 | def apply(title: String, body: Option[String], assignee: Option[String], milestone: Option[Int], labels: Seq[String]): IssueInput = 150 | IssueInput(Some(title), body, assignee, milestone, labels, None) 151 | } 152 | 153 | sealed abstract class IssueAction(val name: String) { 154 | override def toString = name 155 | } 156 | 157 | object IssueAction { 158 | case object assigned extends IssueAction("assigned") 159 | case object unassigned extends IssueAction("unassigned") 160 | case object labeled extends IssueAction("labeled") 161 | case object unlabeled extends IssueAction("unlabeled") 162 | case object opened extends IssueAction("opened") 163 | case object edited extends IssueAction("edited") 164 | case object closed extends IssueAction("closed") 165 | case object reopened extends IssueAction("reopened") 166 | 167 | val values = Array( 168 | assigned, 169 | unassigned, 170 | labeled, 171 | unlabeled, 172 | opened, 173 | closed, 174 | reopened 175 | ) 176 | 177 | def fromString(str: String) = values.filter(_.name == str).head 178 | } 179 | 180 | case class Issue(value: JValue) extends AbstractJson(value) { 181 | def url = get("url") 182 | def labels_url = get("labels_url") 183 | def comments_url = get("comments_url") 184 | def events_url = get("events_url") 185 | def html_url = get("html_url") 186 | def id = get("id").toLong 187 | def number = get("number").toLong 188 | def title = get("title") 189 | 190 | lazy val user = new User(value \ "user") 191 | lazy val labels = (value \ "labels") match { 192 | case JArray(arr) => arr.map(new Label(_)) 193 | case _ => Nil 194 | } 195 | 196 | def state = IssueState.fromString(get("state")) 197 | def locked = boolean("locked") 198 | 199 | lazy val assignee = objectOpt("assignee")(v => User(v)) 200 | lazy val milestone = objectOpt("milestone")(v => Milestone(v)) 201 | 202 | def comments = get("comments").toInt 203 | def created_at = getDate("created_at") 204 | def updated_at = getDate("updated_at") 205 | def closed_at = dateOpt("closed_at") 206 | def body = get("body") 207 | 208 | lazy val closed_by = objectOpt("closed_by")(v => User(v)) 209 | 210 | lazy val repository = new Repository(value \ "repository") 211 | } 212 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/models/Label.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.models 2 | 3 | import org.json4s.JValue 4 | import org.json4s.jackson.JsonMethods 5 | 6 | case class Label(value: JValue) extends AbstractJson(value) { 7 | def url = get("url") 8 | def name = get("name") 9 | def color = get("color") 10 | } 11 | 12 | case class LabelInput(name: String, color: String) extends AbstractInput 13 | 14 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/models/LanguageList.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.models 2 | 3 | import org.json4s.{JValue, JObject} 4 | import org.json4s.jackson.JsonMethods 5 | import codecheck.github.utils.Json4s.formats 6 | 7 | case class LanguageItem(name: String, bytes: Long, rate: Double) 8 | 9 | case class LanguageList(value: JValue) extends AbstractJson(value) { 10 | 11 | lazy val items: List[LanguageItem] = { 12 | value match { 13 | case JObject(fields) => 14 | val temp = fields.map { case (name, bytes) => 15 | LanguageItem(name, bytes.extract[Long], 0.0) 16 | } 17 | val total = temp.map(_.bytes).sum 18 | temp.map { v => 19 | val r = v.bytes.toDouble / total 20 | v.copy(rate = r) 21 | } 22 | case _ => Nil 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/models/Milestone.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.models 2 | 3 | import org.json4s.JValue 4 | import org.json4s.JsonDSL._ 5 | import org.joda.time.{DateTime, DateTimeZone} 6 | 7 | sealed abstract class MilestoneState(val name: String) { 8 | override def toString = name 9 | } 10 | 11 | object MilestoneState { 12 | case object open extends MilestoneState("open") 13 | case object closed extends MilestoneState("closed") 14 | case object all extends MilestoneState("all") 15 | 16 | val values = Array(open, closed, all) 17 | 18 | def fromString(str: String) = values.filter(_.name == str).head 19 | } 20 | 21 | sealed abstract class MilestoneSort(val name: String) { 22 | override def toString = name 23 | } 24 | 25 | object MilestoneSort { 26 | case object due_date extends MilestoneSort("due_date") 27 | case object completeness extends MilestoneSort("completeness") 28 | 29 | val values = Array(due_date, completeness) 30 | 31 | def fromString(str: String) = values.filter(_.name == str).head 32 | } 33 | 34 | case class MilestoneListOption( 35 | state: MilestoneState = MilestoneState.open, 36 | sort: MilestoneSort = MilestoneSort.due_date, 37 | direction: SortDirection = SortDirection.asc 38 | ) 39 | 40 | case class MilestoneInput( 41 | title: Option[String] = None, 42 | state: Option[MilestoneState] = None, 43 | description: Option[String] = None, 44 | due_on: Option[DateTime] = None 45 | ) extends AbstractInput { 46 | override val value: JValue = { 47 | ("title" -> title) ~ 48 | ("state" -> state.map(_.name)) ~ 49 | ("description" -> description) ~ 50 | ("due_on" -> due_on.map(_.toDateTime(DateTimeZone.UTC).toString("yyyy-MM-dd'T'HH:mm:ss'Z'"))) 51 | } 52 | } 53 | 54 | object MilestoneInput { 55 | def apply(title: String): MilestoneInput = 56 | MilestoneInput(Some(title), None, None, None) 57 | def apply(title: String, description: String): MilestoneInput = 58 | MilestoneInput(Some(title), None, Some(description)) 59 | def apply(title: String, due_on: DateTime): MilestoneInput = 60 | MilestoneInput(Some(title), None, None, Some(due_on)) 61 | def apply(title: String, description: String, due_on: DateTime): MilestoneInput = 62 | MilestoneInput(Some(title), None, Some(description), Some(due_on)) 63 | } 64 | 65 | case class Milestone(value: JValue) extends AbstractJson(value) { 66 | def url = get("url") 67 | def id = get("id").toLong 68 | def number = get("number").toInt 69 | lazy val state = MilestoneState.fromString(get("state")) 70 | def title = get("title") 71 | def description = opt("description") 72 | lazy val creator = User(value \ "creator") 73 | def open_issues = get("open_issues").toInt 74 | def closed_issues = get("closed_issues").toInt 75 | def created_at = getDate("created_at") 76 | def updated_at = getDate("updated_at") 77 | def closed_at = dateOpt("closed_at") 78 | def due_on = dateOpt("due_on") 79 | } 80 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/models/OAuthErrorResponse.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.models 2 | 3 | import org.json4s.JValue 4 | 5 | case class OAuthErrorResponse(value: JValue) extends AbstractJson(value) { 6 | def error = get("error") 7 | def error_description = get("error_description") 8 | def error_uri = get("error_uri") 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/models/Organization.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.models 2 | 3 | import org.json4s.JValue 4 | import org.json4s.JsonDSL._ 5 | 6 | /* 7 | ## Github API response differently to organization requests depending on your involvement 8 | 9 | # listOwnOrganization and listUserOrganization returns only fields in the Organization class 10 | 11 | # updateOrganization will return all fields as you must be a member to update 12 | 13 | # getOrganization will return the fields in OrganizationDetail if you are a member 14 | If you are not a member of the organization, the following fields will NOT be returned 15 | - total_private_repos (int) 16 | - owned_private_repos (int) 17 | - private_gists (int) 18 | - disk_usage (long) 19 | - collaborators (int) 20 | - billing_email (string) 21 | - plan (object -> name, space, private_repos) 22 | 23 | ## The following fields are optional (can be empty) 24 | - name (string) 25 | - email (string) 26 | - description (string) 27 | - location (string) 28 | - blog (string) 29 | - company (string) 30 | */ 31 | 32 | 33 | class Organization(value: JValue) extends AbstractJson(value) { 34 | def login = get("login") 35 | def id = get("id").toLong 36 | def url = get("url") 37 | def repos_url = get("repos_url") 38 | def events_url = get("events_url") 39 | def members_url = get("members_url") 40 | def public_members_url = get("public_members_url") 41 | def avatar_url = get("avatar_url") 42 | def description = get("description") 43 | } 44 | 45 | case class OrganizationDetail(value: JValue) extends Organization(value) { 46 | def name = get("name") 47 | def company = opt("company") 48 | def blog = get("blog") 49 | def location = get("location") 50 | def email = get("email") 51 | def public_repos = get("public_repos").toInt 52 | def public_gists = get("public_gists").toInt 53 | def followers = get("followers").toInt 54 | def following = get("following").toInt 55 | def html_url = get("html_url") 56 | def created_at = getDate("created_at") 57 | def updated_at = getDate("updated_at") 58 | def `type` = get("type") 59 | } 60 | 61 | case class OrganizationInput( 62 | name: Option[String] = None, 63 | company: Option[String] = None, 64 | description: Option[String] = None, 65 | location: Option[String] = None, 66 | email: Option[String] = None, 67 | billing_email: Option[String] = None 68 | ) extends AbstractInput 69 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/models/PullRequest.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.models 2 | 3 | import org.json4s.JValue 4 | 5 | case class PullRequestInput( 6 | title: String, 7 | head: String, 8 | base: String, 9 | body: Option[String] 10 | ) extends AbstractInput 11 | 12 | sealed abstract class PullRequestAction(val name: String) { 13 | override def toString = name 14 | } 15 | 16 | object PullRequestAction { 17 | case object assigned extends PullRequestAction("assigned") 18 | case object unassigned extends PullRequestAction("unassigned") 19 | case object review_requested extends PullRequestAction("review_requested") 20 | case object review_request_removed extends PullRequestAction("review_request_removed") 21 | case object labeled extends PullRequestAction("labeled") 22 | case object unlabeled extends PullRequestAction("unlabeled") 23 | case object opened extends PullRequestAction("opened") 24 | case object edited extends PullRequestAction("edited") 25 | case object closed extends PullRequestAction("closed") 26 | case object reopened extends PullRequestAction("reopened") 27 | case object synchronize extends PullRequestAction("synchronize") 28 | 29 | val values = Array( 30 | assigned, 31 | unassigned, 32 | review_requested, 33 | review_request_removed, 34 | labeled, 35 | unlabeled, 36 | opened, 37 | edited, 38 | closed, 39 | reopened, 40 | synchronize 41 | ) 42 | 43 | def fromString(str: String) = values.filter(_.name == str).head 44 | } 45 | 46 | case class PullRequestListOption( 47 | state: IssueStateFilter = IssueStateFilter.open, 48 | head: Option[String] = None, 49 | base: Option[String] = None, 50 | sort: IssueSort = IssueSort.created, 51 | direction: SortDirection = SortDirection.desc 52 | ) 53 | 54 | case class PullRequestRef(value: JValue) extends AbstractJson(value) { 55 | def label = get("label") 56 | def ref = get("ref") 57 | def sha = get("sha") 58 | lazy val user = User(value \ "user") 59 | lazy val repo = objectOpt("repo")(Repository(_)) 60 | } 61 | 62 | case class PullRequest(value: JValue) extends AbstractJson(value) { 63 | def number = get("number").toLong 64 | def body = get("body") 65 | lazy val user = User(value \ "user") 66 | def state = IssueState.fromString(get("state")) 67 | def title = get("title") 68 | lazy val head = PullRequestRef(value \ "head") 69 | lazy val base = PullRequestRef(value \ "base") 70 | def mergeable = booleanOpt("mergeable") 71 | def merged = booleanOpt("merged") 72 | def merge_commit_sha = get("merge_commit_sha") 73 | def merged_by = objectOpt("merged_by")(v => User(v)) 74 | def comments = opt("comments").map(_.toLong) 75 | def commits = opt("commits").map(_.toLong) 76 | def additions = opt("additions").map(_.toLong) 77 | def deletions = opt("deletions").map(_.toLong) 78 | def changed_files = opt("changed_files").map(_.toLong) 79 | def maintainer_can_modify = booleanOpt("maintainer_can_modify") 80 | } 81 | 82 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/models/PullRequestReview.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github 2 | package models 3 | 4 | import org.json4s.JsonDSL._ 5 | import org.json4s.JNull 6 | import org.json4s.JValue 7 | 8 | case class PullRequestReviewInput( 9 | body: Option[String] = None, 10 | event: Option[PullRequestReviewStateInput] = None, 11 | comments: Seq[PullRequestReviewCommentInput] = Seq.empty[PullRequestReviewCommentInput] 12 | ) extends AbstractInput { 13 | override val value: JValue = { 14 | ("body" -> body) ~ 15 | ("event" -> event.map(_.name)) ~ 16 | ("comments" -> comments.map(_.value)) 17 | } 18 | } 19 | 20 | case class PullRequestReviewCommentInput( 21 | path: String, 22 | position: Long, 23 | body: String 24 | ) extends AbstractInput 25 | 26 | sealed abstract class PullRequestReviewAction(val name: String) { 27 | override def toString = name 28 | } 29 | 30 | object PullRequestReviewAction { 31 | case object submitted extends PullRequestReviewAction("submitted") 32 | case object edited extends PullRequestReviewAction("edited") 33 | case object dismissed extends PullRequestReviewAction("dismissed") 34 | 35 | val values = Array( 36 | submitted, 37 | edited, 38 | dismissed 39 | ) 40 | 41 | def fromString(str: String) = values.filter(_.name == str).head 42 | } 43 | 44 | sealed abstract class PullRequestReviewState(val name: String) { 45 | override def toString = name 46 | } 47 | 48 | object PullRequestReviewState { 49 | case object approved extends PullRequestReviewState("approved") 50 | case object dismissed extends PullRequestReviewState("dismissed") 51 | case object pending extends PullRequestReviewState("pending") 52 | case object changes_requested extends PullRequestReviewState("changes_requested") 53 | 54 | val values = Array( 55 | approved, 56 | dismissed, 57 | pending, 58 | changes_requested 59 | ) 60 | 61 | def fromString(str: String) = values.filter(_.name == str.toLowerCase).head 62 | } 63 | 64 | sealed abstract class PullRequestReviewStateInput(val name: String) 65 | 66 | object PullRequestReviewStateInput { 67 | case object APPROVE extends PullRequestReviewStateInput("APPROVE") 68 | case object COMMENT extends PullRequestReviewStateInput("COMMENT") 69 | case object PENDING extends PullRequestReviewStateInput("PENDING") 70 | case object REQUEST_CHANGES extends PullRequestReviewStateInput("REQUEST_CHANGES") 71 | 72 | val values = Array( 73 | APPROVE, 74 | COMMENT, 75 | PENDING, 76 | REQUEST_CHANGES 77 | ) 78 | 79 | def fromString(str: String) = values.filter(_.name == str).head 80 | } 81 | 82 | case class PullRequestReview(value: JValue) extends AbstractJson(value) { 83 | def id = get("id").toLong 84 | def body = get("body") 85 | def commit_id = get("commit_id") 86 | lazy val user = User(value \ "user") 87 | def state = PullRequestReviewState.fromString(get("state")) 88 | def submitted_at = dateOpt("submitted_at") 89 | } 90 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/models/Repository.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.models 2 | 3 | import java.net.URL 4 | import org.json4s.JValue 5 | import codecheck.github.utils.ToDo 6 | 7 | sealed abstract class RepositoryListType(val name: String) { 8 | override def toString = name 9 | } 10 | 11 | object RepositoryListType { 12 | case object all extends RepositoryListType("all") 13 | case object owner extends RepositoryListType("owner") 14 | case object public extends RepositoryListType("public") 15 | case object private_ extends RepositoryListType("private") 16 | case object member extends RepositoryListType("member") 17 | case object forks extends RepositoryListType("forks") 18 | case object sources extends RepositoryListType("sources") 19 | 20 | val values = Array(all, owner, public, private_, member, forks, sources) 21 | 22 | def fromString(str: String) = if (str == "private") { 23 | private_ 24 | } else { 25 | values.filter(_.name == str).head 26 | } 27 | } 28 | 29 | sealed abstract class RepositorySort(val name: String) { 30 | override def toString = name 31 | } 32 | 33 | object RepositorySort { 34 | case object created extends RepositorySort("created") 35 | case object updated extends RepositorySort("updated") 36 | case object pushed extends RepositorySort("pushed") 37 | case object full_name extends RepositorySort("full_name") 38 | 39 | val values = Array(created, updated, pushed, full_name) 40 | 41 | def fromString(str: String) = values.filter(_.name == str).head 42 | } 43 | 44 | case class RepositoryListOption( 45 | listType: RepositoryListType = RepositoryListType.all, 46 | sort: RepositorySort = RepositorySort.full_name, 47 | direction: SortDirection = SortDirection.asc 48 | ) 49 | 50 | case class RepositoryInput( 51 | name: String, 52 | description: Option[String] = None, 53 | homepage: Option[URL] = None, 54 | `private`: Boolean = false, 55 | has_issues: Boolean = true, 56 | has_wiki: Boolean = true, 57 | has_downloads: Boolean = true, 58 | team_id: Option[Int] = None, 59 | auto_init: Boolean = false, 60 | gitignore_template: Option[String] = None, 61 | license_template: Option[String] = None 62 | ) extends AbstractInput 63 | 64 | case class Repository(value: JValue) extends AbstractJson(value) { 65 | def id = get("id").toLong 66 | def name = get("name") 67 | def full_name = get("full_name") 68 | def url = get("url") 69 | def language = get("language") 70 | def stargazers_count = get("stargazers_count").toLong 71 | 72 | def description = opt("description") 73 | def open_issues_count = get("open_issues_count").toLong 74 | def `private` = boolean("private") 75 | 76 | lazy val permissions = Permissions(value \ "permissions") 77 | lazy val owner = User(value \ "owner") 78 | } 79 | 80 | case class Permissions(value: JValue) extends AbstractJson(value) { 81 | def admin = boolean("admin") 82 | def push = boolean("push") 83 | def pull = boolean("pull") 84 | } 85 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/models/ReviewRequest.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.models 2 | 3 | import org.json4s.JValue 4 | 5 | case class ReviewRequest(value: JValue) extends AbstractJson(value) { 6 | def id = get("id").toLong 7 | def number = get("number").toLong 8 | } 9 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/models/Search.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.models 2 | 3 | import org.json4s.JValue 4 | import org.json4s.JArray 5 | 6 | sealed trait SearchSort { 7 | def name: String 8 | override def toString = name 9 | } 10 | 11 | sealed abstract class SearchRepositorySort(val name: String) extends SearchSort 12 | 13 | object SearchRepositorySort { 14 | case object stars extends SearchRepositorySort("stars") 15 | case object forks extends SearchRepositorySort("forks") 16 | case object updated extends SearchRepositorySort("updated") 17 | 18 | val values = Array(stars, forks, updated) 19 | 20 | def fromString(str: String) = values.filter(_.name == str).head 21 | } 22 | 23 | sealed abstract class SearchCodeSort(val name: String) extends SearchSort 24 | 25 | object SearchCodeSort { 26 | case object indexed extends SearchCodeSort("indexed") 27 | 28 | val values = Array(indexed) 29 | 30 | def fromString(str: String) = values.filter(_.name == str).head 31 | } 32 | 33 | sealed abstract class SearchIssueSort(val name: String) extends SearchSort 34 | 35 | object SearchIssueSort { 36 | case object created extends SearchIssueSort("created") 37 | case object updated extends SearchIssueSort("updated") 38 | case object comments extends SearchIssueSort("comments") 39 | 40 | val values = Array(created, updated, comments) 41 | 42 | def fromString(str: String) = values.filter(_.name == str).head 43 | } 44 | 45 | sealed abstract class SearchUserSort(val name: String) extends SearchSort 46 | 47 | object SearchUserSort { 48 | case object followers extends SearchUserSort("followers") 49 | case object repositories extends SearchUserSort("repositories") 50 | case object joined extends SearchUserSort("joined") 51 | 52 | val values = Array(followers, repositories, joined) 53 | 54 | def fromString(str: String) = values.filter(_.name == str).head 55 | } 56 | 57 | sealed trait SearchInput extends AbstractInput { 58 | def q: String 59 | def sort: Option[SearchSort] 60 | def order: SortDirection 61 | def query = s"?q=$q" + sort.map(sortBy => s"&sort=$sortBy&order=$order").getOrElse("") 62 | } 63 | 64 | case class SearchRepositoryInput ( 65 | val q: String, 66 | val sort: Option[SearchRepositorySort] = None, 67 | val order: SortDirection = SortDirection.desc 68 | ) extends SearchInput 69 | 70 | case class SearchRepositoryResult(value: JValue) extends AbstractJson(value) { 71 | def total_count: Long = get("total_count").toLong 72 | def incomplete_results: Boolean = boolean("incomplete_results") 73 | lazy val items = (value \ "items") match { 74 | case JArray(arr) => arr.map(Repository(_)) 75 | case _ => Nil 76 | } 77 | } 78 | 79 | case class SearchCodeInput ( 80 | q: String, 81 | sort: Option[SearchCodeSort] = None, 82 | order: SortDirection = SortDirection.desc 83 | ) extends SearchInput 84 | 85 | case class SearchCodeItem(value: JValue) extends AbstractJson(value) { 86 | def name: String = get("name") 87 | def path: String = get("path") 88 | def sha: String = get("sha") 89 | def url: String = get("url") 90 | def git_url: String = get("git_url") 91 | def html_url: String = get("html_url") 92 | def score: Double = get("score").toDouble 93 | lazy val repository = Repository(value \ "repository") 94 | } 95 | 96 | case class SearchCodeResult(value: JValue) extends AbstractJson(value) { 97 | def total_count: Long = get("total_count").toLong 98 | def incomplete_results: Boolean = boolean("incomplete_results") 99 | lazy val items = (value \ "items") match { 100 | case JArray(arr) => arr.map(SearchCodeItem(_)) 101 | case _ => Nil 102 | } 103 | } 104 | 105 | case class SearchIssueInput ( 106 | q: String, 107 | sort: Option[SearchIssueSort] = None, 108 | order: SortDirection = SortDirection.desc 109 | ) extends SearchInput 110 | 111 | case class SearchIssueResult(value: JValue) extends AbstractJson(value) { 112 | def total_count: Long = get("total_count").toLong 113 | def incomplete_results: Boolean = boolean("incomplete_results") 114 | lazy val items = (value \ "items") match { 115 | case JArray(arr) => arr.map(Issue(_)) 116 | case _ => Nil 117 | } 118 | } 119 | 120 | case class SearchUserInput ( 121 | q: String, 122 | sort: Option[SearchUserSort] = None, 123 | order: SortDirection = SortDirection.desc 124 | ) extends SearchInput 125 | 126 | case class SearchUserResult(value: JValue) extends AbstractJson(value) { 127 | def total_count: Long = get("total_count").toLong 128 | def incomplete_results: Boolean = boolean("incomplete_results") 129 | lazy val items = (value \ "items") match { 130 | case JArray(arr) => arr.map(User(_)) 131 | case _ => Nil 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/models/SortDirection.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.models 2 | 3 | sealed abstract class SortDirection(val name: String) { 4 | override def toString = name 5 | } 6 | 7 | object SortDirection { 8 | case object asc extends SortDirection("asc") 9 | case object desc extends SortDirection("desc") 10 | 11 | val values = Array(asc, desc) 12 | 13 | def fromString(str: String) = values.filter(_.name == str).head 14 | } 15 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/models/Status.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github 2 | package models 3 | 4 | import org.json4s.JValue 5 | import org.json4s.JArray 6 | 7 | sealed abstract class StatusState(override val toString: String) 8 | 9 | object StatusState { 10 | case object pending extends StatusState("pending") 11 | case object success extends StatusState("success") 12 | case object error extends StatusState("error") 13 | case object failure extends StatusState("failure") 14 | 15 | val values = Array(pending, success, error, failure) 16 | 17 | def fromString(str: String) = values.filter(_.toString == str).head 18 | } 19 | 20 | case class Status(value: JValue) extends AbstractJson(value) { 21 | def state = StatusState.fromString(get("state")) 22 | def target_url = opt("target_url") 23 | def description = opt("description") 24 | def context = get("context") 25 | def id = get("id").toLong 26 | def url = get("url") 27 | def created_at = getDate("created_at") 28 | def updated_at = getDate("updated_at") 29 | lazy val assignee = objectOpt("assignee")(v => User(v)) 30 | } 31 | 32 | case class StatusInput( 33 | state: StatusState, 34 | target_url: Option[String] = None, 35 | description: Option[String] = None, 36 | context: Option[String] = None 37 | ) extends AbstractInput { 38 | import org.json4s.JsonDSL._ 39 | 40 | override val value: JValue = 41 | ("state" -> state.toString) ~ 42 | ("target_url" -> target_url) ~ 43 | ("description" -> description) ~ 44 | ("context" -> context) 45 | } 46 | 47 | case class CombinedStatus(value: JValue) extends AbstractJson(value) { 48 | def state = StatusState.fromString(get("state")) 49 | def sha = get("sha") 50 | lazy val statuses = (value \ "statuses") match { 51 | case JArray(arr) => arr.map(Status(_)) 52 | case _ => Nil 53 | } 54 | 55 | def repository = Repository(value \ "repository") 56 | def commit_url = get("commit_url") 57 | def total_count = get("total_count").toLong 58 | def url = get("url") 59 | } 60 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/models/User.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.models 2 | 3 | import org.json4s.JValue 4 | import codecheck.github.utils.ToDo 5 | import org.json4s.JsonDSL._ 6 | 7 | case class User(value: JValue) extends AbstractJson(value) { 8 | def login: String = get("login") 9 | def id: Long = get("id").toLong 10 | def email: Option[String] = opt("email") 11 | def name: Option[String] = opt("name") 12 | def blog: Option[String] = opt("blog") 13 | def company: Option[String] = opt("company") 14 | def location: Option[String] = opt("location") 15 | def hireable: Boolean = booleanOpt("hireable").getOrElse(false) 16 | def bio: Option[String] = opt("bio") 17 | } 18 | 19 | /*case class UserInput extends ToDo*/ 20 | case class UserInput ( 21 | name: Option[String] = None, 22 | email: Option[String] = None, 23 | blog: Option[String] = None, 24 | company: Option[String] = None, 25 | location: Option[String] = None, 26 | hireable: Option[Boolean] = None, 27 | bio: Option[String] = None 28 | ) extends AbstractInput 29 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/models/Webhook.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.models 2 | 3 | import org.json4s.JValue 4 | 5 | class Webhook(value: JValue) extends AbstractJson(value) { 6 | def id: Long = get("id").toLong 7 | def url = get("url") 8 | def test_url = get("test_url") 9 | def ping_url = get("ping_url") 10 | def name = get("name") 11 | def events = seq("events") 12 | def active = boolean("active") 13 | def config = WebhookConfig(opt("config.url"), opt("config.content_type"), opt("config.secret"), opt("config.insecure_ssl").map(_ == "1")); 14 | def last_response = WebhookResponse(value \ "last_response") 15 | def updated_at = getDate("updated_at") 16 | def created_at = getDate("created_at") 17 | } 18 | 19 | case class WebhookConfig( 20 | url: Option[String], 21 | content_type: Option[String], 22 | secret: Option[String], 23 | insecure_ssl: Option[Boolean] 24 | ) extends AbstractInput 25 | 26 | object WebhookConfig { 27 | def apply( 28 | url: String, 29 | content_type: String = "json", 30 | secret: Option[String] = None, 31 | insecure_ssl: Boolean = false 32 | ): WebhookConfig = WebhookConfig(Some(url), Some(content_type), secret, Some(insecure_ssl)) 33 | } 34 | 35 | case class WebhookCreateInput( 36 | name: String, 37 | config: WebhookConfig, 38 | active: Boolean = true, 39 | events: Seq[String] = Seq("push"), 40 | add_events: Seq[String] = Nil, 41 | remove_events: Seq[String] = Nil 42 | ) extends AbstractInput 43 | 44 | case class WebhookUpdateInput( 45 | config: Option[WebhookConfig] = None, 46 | events: Option[Seq[String]] = None, 47 | add_events: Option[Seq[String]] = None, 48 | remove_events: Option[Seq[String]] = None, 49 | active: Option[Boolean] = None 50 | ) extends AbstractInput 51 | 52 | case class WebhookResponse(value: JValue) extends AbstractJson(value) { 53 | def code = get("code") 54 | def status = get("status") 55 | def message = get("message") 56 | } 57 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/operations/BranchOp.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.operations 2 | 3 | import java.net.URLEncoder 4 | import scala.concurrent.Future 5 | import scala.concurrent.ExecutionContext.Implicits.global 6 | import org.json4s.JArray 7 | import org.json4s.JString 8 | import org.json4s.JNothing 9 | 10 | import codecheck.github.api.GitHubAPI 11 | import codecheck.github.exceptions.NotFoundException 12 | import codecheck.github.models.BranchListItem 13 | import codecheck.github.models.Branch 14 | 15 | trait BranchOp { 16 | self: GitHubAPI => 17 | 18 | def listBranches(owner: String, repo: String): Future[List[BranchListItem]] = { 19 | exec("GET", s"/repos/$owner/$repo/branches").map( 20 | _.body match { 21 | case JArray(arr) => arr.map(v => BranchListItem(v)) 22 | case _ => throw new IllegalStateException() 23 | } 24 | ) 25 | } 26 | 27 | def getBranch(owner: String, repo: String, branch: String): Future[Option[Branch]] = { 28 | exec("GET", s"/repos/$owner/$repo/branches/${encode(branch)}", fail404=false).map{ res => 29 | res.statusCode match { 30 | case 404 => None 31 | case 200 => Some(Branch(res.body)) 32 | } 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/operations/CollaboratorOp.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.operations 2 | 3 | import scala.concurrent.Future 4 | import scala.concurrent.ExecutionContext.Implicits.global 5 | import org.json4s.JArray 6 | 7 | import codecheck.github.api.GitHubAPI 8 | import codecheck.github.exceptions.NotFoundException 9 | import codecheck.github.models.Collaborator 10 | 11 | trait CollaboratorOp { 12 | self: GitHubAPI => 13 | 14 | def listCollaborators(owner: String,repo:String): Future[List[Collaborator]] = { 15 | val path = s"/repos/${owner}/${repo}/collaborators" 16 | exec("GET",path).map( 17 | _.body match { 18 | case JArray(arr) => arr.map(v => Collaborator(v)) 19 | case _ => throw new IllegalStateException() 20 | } 21 | ) 22 | } 23 | 24 | def isCollaborator(owner: String, repo: String, name: String): Future[Boolean] = { 25 | val path = s"/repos/${owner}/${repo}/collaborators/" + encode(name) 26 | exec("GET", path, fail404 = false).map { res => 27 | res.statusCode match { 28 | case 404 => false 29 | case 204 => true 30 | } 31 | } 32 | } 33 | 34 | def addCollaborator(owner: String, repo: String, name: String): Future[Boolean] = { 35 | val path = s"/repos/${owner}/${repo}/collaborators/" + encode(name) 36 | exec("PUT", path).map { 37 | _.statusCode == 204 38 | } 39 | } 40 | 41 | def removeCollaborator(owner: String, repo: String, name: String): Future[Boolean] = { 42 | val path = s"/repos/${owner}/${repo}/collaborators/" + encode(name) 43 | exec("DELETE", path).map { 44 | _.statusCode == 204 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/operations/IssueOp.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.operations 2 | 3 | import java.net.URLEncoder 4 | import scala.concurrent.Future 5 | import scala.concurrent.ExecutionContext.Implicits.global 6 | import org.json4s.JArray 7 | import org.json4s.JString 8 | import org.json4s.JNothing 9 | 10 | import codecheck.github.api.GitHubAPI 11 | import codecheck.github.exceptions.NotFoundException 12 | import codecheck.github.models.IssueInput 13 | import codecheck.github.models.Issue 14 | import codecheck.github.models.IssueListOption 15 | import codecheck.github.models.IssueListOption4Repository 16 | 17 | trait IssueOp { 18 | self: GitHubAPI => 19 | 20 | private def doList(path: String): Future[List[Issue]] = { 21 | exec("GET", path).map( 22 | _.body match { 23 | case JArray(arr) => arr.map(v => Issue(v)) 24 | case _ => throw new IllegalStateException() 25 | } 26 | ) 27 | } 28 | 29 | //Only listAll/User/OrgIssues return Repository object 30 | def listAllIssues(option: IssueListOption = IssueListOption()): Future[List[Issue]] = 31 | doList("/issues" + option.q) 32 | 33 | def listUserIssues(option: IssueListOption = IssueListOption()): Future[List[Issue]] = 34 | doList("/user/issues" + option.q) 35 | 36 | def listOrgIssues(org: String, option: IssueListOption = IssueListOption()): Future[List[Issue]] = 37 | doList(s"/orgs/$org/issues" + option.q) 38 | 39 | def listRepositoryIssues(owner: String, repo: String, option: IssueListOption4Repository = IssueListOption4Repository()): Future[List[Issue]] = 40 | doList(s"/repos/$owner/$repo/issues" + option.q) 41 | 42 | def getIssue(owner: String, repo: String, number: Long): Future[Option[Issue]] = 43 | exec("GET", s"/repos/$owner/$repo/issues/$number", fail404=false).map(res => 44 | res.statusCode match { 45 | case 404 => None 46 | case 200 => Some(Issue(res.body)) 47 | } 48 | ) 49 | 50 | def createIssue(owner: String, repo: String, input: IssueInput): Future[Issue] = { 51 | val path = s"/repos/$owner/$repo/issues" 52 | exec("POST", path, input.value).map { result => 53 | new Issue(result.body) 54 | } 55 | } 56 | 57 | def editIssue(owner: String, repo: String, number: Long, input: IssueInput): Future[Issue] = { 58 | val path = s"/repos/$owner/$repo/issues/$number" 59 | exec("PATCH", path, input.value).map { result => 60 | new Issue(result.body) 61 | } 62 | } 63 | 64 | def assign(owner: String, repo: String, number: Long, assignee: String): Future[Issue] = { 65 | editIssue(owner, repo, number, IssueInput(assignee=Some(assignee))) 66 | } 67 | 68 | def unassign(owner: String, repo: String, number: Long): Future[Issue] = { 69 | editIssue(owner, repo, number, IssueInput(assignee=Some(""))) 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/operations/LabelOp.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.operations 2 | 3 | import scala.concurrent.Future 4 | import scala.concurrent.ExecutionContext.Implicits.global 5 | import org.json4s.JArray 6 | import org.json4s.JString 7 | import org.json4s.JNothing 8 | 9 | import codecheck.github.api.GitHubAPI 10 | import codecheck.github.exceptions.NotFoundException 11 | import codecheck.github.models.Label 12 | import codecheck.github.models.LabelInput 13 | 14 | trait LabelOp { 15 | self: GitHubAPI => 16 | 17 | private def doLabels(method: String, owner: String, repo: String, number: Long, labels: Seq[String]): Future[List[Label]] = { 18 | val path = s"/repos/$owner/$repo/issues/$number/labels" 19 | val body = if (method == "GET") { 20 | JNothing 21 | } else { 22 | JArray(labels.map(JString(_)).toList) 23 | } 24 | exec(method, path, body).map { 25 | _.body match { 26 | case JArray(arr) => arr.map(v => Label(v)) 27 | case _ => throw new IllegalStateException() 28 | } 29 | } 30 | } 31 | 32 | def addLabels(owner: String, repo: String, number: Long, labels: String*): Future[List[Label]] = { 33 | doLabels("POST", owner, repo, number, labels) 34 | } 35 | 36 | def replaceLabels(owner: String, repo: String, number: Long, labels: String*): Future[List[Label]] = { 37 | doLabels("PUT", owner, repo, number, labels) 38 | } 39 | 40 | def removeAllLabels(owner: String, repo: String, number: Long): Future[List[Label]] = { 41 | doLabels("PUT", owner, repo, number, Nil) 42 | } 43 | 44 | def removeLabel(owner: String, repo: String, number: Long, label: String): Future[List[Label]] = { 45 | val path = s"/repos/$owner/$repo/issues/$number/labels/" + encode(label) 46 | exec("DELETE", path).map { 47 | _.body match { 48 | case JArray(arr) => arr.map(v => Label(v)) 49 | case _ => throw new IllegalStateException() 50 | } 51 | } 52 | } 53 | 54 | def listLabels(owner: String, repo: String, number: Long): Future[List[Label]] = { 55 | doLabels("GET", owner, repo, number, Nil) 56 | } 57 | 58 | def listLabelDefs(owner: String, repo: String): Future[List[Label]] = { 59 | val path = s"/repos/$owner/$repo/labels" 60 | exec("GET", path).map { 61 | _.body match { 62 | case JArray(arr) => arr.map(v => Label(v)) 63 | case _ => throw new IllegalStateException() 64 | } 65 | } 66 | } 67 | 68 | def getLabelDef(owner: String, repo: String, label: String): Future[Option[Label]] = { 69 | val path = s"/repos/$owner/$repo/labels/" + encode(label) 70 | exec("GET", path, fail404=false).map(res => 71 | res.statusCode match { 72 | case 404 => None 73 | case 200 => Some(Label(res.body)) 74 | } 75 | ) 76 | } 77 | 78 | def createLabelDef(owner: String, repo: String, label: LabelInput): Future[Label] = { 79 | val path = s"/repos/$owner/$repo/labels" 80 | exec("POST", path, label.value).map(res => Label(res.body)) 81 | } 82 | 83 | def updateLabelDef(owner: String, repo: String, name: String, label: LabelInput): Future[Label] = { 84 | val path = s"/repos/$owner/$repo/labels/" + encode(name) 85 | exec("PATCH", path, label.value).map(res => Label(res.body)) 86 | } 87 | 88 | def removeLabelDef(owner: String, repo: String, name: String): Future[Boolean] = { 89 | val path = s"/repos/$owner/$repo/labels/" + encode(name) 90 | exec("DELETE", path).map { 91 | _.statusCode == 204 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/operations/MilestoneOp.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.operations 2 | 3 | import scala.concurrent.Future 4 | import scala.concurrent.ExecutionContext.Implicits.global 5 | import org.json4s.JArray 6 | import org.json4s.JString 7 | import org.json4s.JNothing 8 | 9 | import codecheck.github.api.GitHubAPI 10 | import codecheck.github.exceptions.NotFoundException 11 | import codecheck.github.models.Milestone 12 | import codecheck.github.models.MilestoneInput 13 | import codecheck.github.models.MilestoneListOption 14 | 15 | trait MilestoneOp { 16 | self: GitHubAPI => 17 | 18 | def listMilestones( 19 | owner: String, 20 | repo: String, 21 | option: MilestoneListOption = MilestoneListOption() 22 | ): Future[List[Milestone]] = { 23 | val path = s"/repos/$owner/$repo/milestones?state=${option.state}&sort=${option.sort}&direction=${option.direction}" 24 | exec("GET", path).map( 25 | _.body match { 26 | case JArray(arr) => arr.map(v => Milestone(v)) 27 | case _ => throw new IllegalStateException() 28 | } 29 | ) 30 | } 31 | 32 | def getMilestone(owner: String, repo: String, number: Int): Future[Option[Milestone]] = { 33 | val path = s"/repos/$owner/$repo/milestones/$number" 34 | exec("GET", path, fail404=false).map { res => 35 | res.statusCode match { 36 | case 404 => None 37 | case 200 => Some(Milestone(res.body)) 38 | } 39 | } 40 | } 41 | 42 | def createMilestone(owner: String, repo: String, input: MilestoneInput): Future[Milestone] = { 43 | val path = s"/repos/$owner/$repo/milestones" 44 | exec("POST", path, input.value).map(res => Milestone(res.body)) 45 | } 46 | 47 | def updateMilestone(owner: String, repo: String, number: Int, input: MilestoneInput): Future[Milestone] = { 48 | val path = s"/repos/$owner/$repo/milestones/$number" 49 | exec("PATCH", path, input.value).map(res => Milestone(res.body)) 50 | } 51 | 52 | def removeMilestone(owner: String, repo: String, number: Int): Future[Boolean] = { 53 | val path = s"/repos/$owner/$repo/milestones/$number" 54 | exec("DELETE", path).map(_.statusCode == 204) 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/operations/OrganizationOp.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.operations 2 | 3 | import scala.concurrent.Future 4 | import scala.concurrent.ExecutionContext.Implicits.global 5 | import org.json4s.JArray 6 | 7 | import codecheck.github.api.GitHubAPI 8 | import codecheck.github.exceptions.NotFoundException 9 | import codecheck.github.models.Organization 10 | import codecheck.github.models.OrganizationDetail 11 | import codecheck.github.models.OrganizationInput 12 | 13 | trait OrganizationOp { 14 | self: GitHubAPI => 15 | 16 | def listOwnOrganizations: Future[List[Organization]] = { 17 | self.exec("GET", s"/user/orgs").map { 18 | _.body match { 19 | case JArray(arr) => arr.map(new Organization(_)) 20 | case _ => throw new IllegalStateException() 21 | } 22 | } 23 | } 24 | 25 | def listUserOrganizations(user: String): Future[List[Organization]] = { 26 | self.exec("GET", s"/users/${user}/orgs").map { 27 | _.body match { 28 | case JArray(arr) => arr.map(new Organization(_)) 29 | case _ => throw new IllegalStateException() 30 | } 31 | } 32 | } 33 | 34 | def getOrganization(org: String): Future[Option[OrganizationDetail]] = { 35 | self.exec("GET", s"/orgs/${org}", fail404=false).map { res => 36 | res.statusCode match { 37 | case 404 => None 38 | case 200 => Some(new OrganizationDetail(res.body)) 39 | } 40 | } 41 | } 42 | 43 | def updateOrganization(org: String, input: OrganizationInput): Future[OrganizationDetail] = { 44 | self.exec("PATCH", s"/orgs/${org}", input.value).map { res => 45 | new OrganizationDetail(res.body) 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/operations/PullRequestOp.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.operations 2 | 3 | import scala.concurrent.Future 4 | import scala.concurrent.ExecutionContext.Implicits.global 5 | import org.json4s.JArray 6 | import org.json4s.JObject 7 | import org.json4s.JString 8 | 9 | import codecheck.github.api.GitHubAPI 10 | import codecheck.github.models.PullRequestInput 11 | import codecheck.github.models.PullRequestListOption 12 | import codecheck.github.models.PullRequest 13 | import codecheck.github.models.ReviewRequest 14 | 15 | trait PullRequestOp { 16 | self: GitHubAPI => 17 | 18 | def listPullRequests( 19 | owner: String, 20 | repo: String, 21 | option: PullRequestListOption = PullRequestListOption() 22 | ): Future[List[PullRequest]] = { 23 | val q = s"?state=${option.state}" + 24 | s"&sort=${option.sort}" + 25 | s"&direction=${option.direction}" + 26 | option.head.map("&head=" + _).getOrElse("") + 27 | option.base.map("&base=" + _).getOrElse("") 28 | 29 | exec("GET", s"/repos/$owner/$repo/pulls$q").map( 30 | _.body match { 31 | case JArray(arr) => arr.map(v => PullRequest(v)) 32 | case _ => throw new IllegalStateException() 33 | } 34 | ) 35 | } 36 | 37 | def getPullRequest(owner: String, repo: String, number: Long): Future[Option[PullRequest]] = { 38 | exec("GET", s"/repos/$owner/$repo/pulls/$number", fail404=false).map( res => 39 | res.statusCode match { 40 | case 404 => None 41 | case 200 => Some(PullRequest(res.body)) 42 | } 43 | ) 44 | } 45 | 46 | def createPullRequest(owner: String, repo: String, input: PullRequestInput): Future[PullRequest] = { 47 | val path = s"/repos/$owner/$repo/pulls" 48 | exec("POST", path, input.value).map { result => 49 | PullRequest(result.body) 50 | } 51 | } 52 | 53 | def closePullRequest(owner: String, repo: String, number: Long): Future[PullRequest] = { 54 | val path = s"/repos/$owner/$repo/pulls/$number" 55 | exec("PATCH", path, JObject(List( 56 | "state" -> JString("close") 57 | ))).map { result => 58 | new PullRequest(result.body) 59 | } 60 | } 61 | 62 | def addReviewRequest(owner: String, repo: String, number: Long, reviewers: String*): Future[ReviewRequest] = { 63 | val path = s"/repos/$owner/$repo/pulls/$number/requested_reviewers" 64 | exec("POST", path, JObject(List( 65 | "reviewers" -> JArray(reviewers.map(JString).toList) 66 | ))).map { result => 67 | ReviewRequest(result.body) 68 | } 69 | } 70 | 71 | def removeReviewRequest(owner: String, repo: String, number: Long, reviewers: String*): Future[Boolean] = { 72 | val path = s"/repos/$owner/$repo/pulls/$number/requested_reviewers" 73 | exec("DELETE", path, JObject(List( 74 | "reviewers" -> JArray(reviewers.map(JString).toList) 75 | ))).map { result => 76 | result.statusCode >= 200 && result.statusCode < 300 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/operations/PullRequestReviewOp.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.operations 2 | 3 | import scala.concurrent.Future 4 | import scala.concurrent.ExecutionContext.Implicits.global 5 | import org.json4s.JArray 6 | import org.json4s.JObject 7 | import org.json4s.JString 8 | 9 | import codecheck.github.api.GitHubAPI 10 | import codecheck.github.models.PullRequestReviewInput 11 | import codecheck.github.models.PullRequestReview 12 | 13 | trait PullRequestReviewOp { 14 | self: GitHubAPI => 15 | 16 | def listPullRequestReviews( 17 | owner: String, 18 | repo: String, 19 | number: Long 20 | ): Future[List[PullRequestReview]] = { 21 | exec("GET", s"/repos/$owner/$repo/pulls/$number/reviews").map( 22 | _.body match { 23 | case JArray(arr) => arr.map(v => PullRequestReview(v)) 24 | case _ => throw new IllegalStateException() 25 | } 26 | ) 27 | } 28 | 29 | def getPullRequestReview(owner: String, repo: String, number: Long, id: Long): Future[Option[PullRequestReview]] = { 30 | val path = s"/repos/$owner/$repo/pulls/$number/reviews/$id" 31 | exec("GET", path, fail404=false).map(res => 32 | res.statusCode match { 33 | case 404 => None 34 | case 200 => Some(PullRequestReview(res.body)) 35 | } 36 | ) 37 | } 38 | 39 | def createPullRequestReview(owner: String, repo: String, number: Long, input: PullRequestReviewInput): Future[PullRequestReview] = { 40 | val path = s"/repos/$owner/$repo/pulls/$number/reviews" 41 | exec("POST", path, input.value).map { result => 42 | PullRequestReview(result.body) 43 | } 44 | } 45 | 46 | def dismissPullRequestReview(owner: String, repo: String, number: Long, id: Long, message: String): Future[PullRequestReview] = { 47 | val path = s"/repos/$owner/$repo/pulls/$number/reviews/$id/dismissals" 48 | exec("PUT", path, JObject(List( 49 | "message" -> JString(message) 50 | ))).map { result => 51 | new PullRequestReview(result.body) 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/operations/RepositoryOp.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.operations 2 | 3 | import scala.concurrent.Future 4 | import scala.concurrent.ExecutionContext.Implicits.global 5 | import org.json4s.{JArray, JString} 6 | 7 | import codecheck.github.api.GitHubAPI 8 | import codecheck.github.exceptions.NotFoundException 9 | import codecheck.github.utils.ToDo 10 | import codecheck.github.models.Repository 11 | import codecheck.github.models.RepositoryInput 12 | import codecheck.github.models.RepositoryListOption 13 | import codecheck.github.models.LanguageList 14 | 15 | trait RepositoryOp { 16 | self: GitHubAPI => 17 | 18 | private def doList(path: String, option: RepositoryListOption): Future[List[Repository]] = { 19 | exec("GET", path + s"?type=${option.listType}&sort=${option.sort}&direction=${option.direction}").map { res => 20 | res.body match { 21 | case JArray(arr) => arr.map(v => Repository(v)) 22 | case _ => throw new IllegalStateException() 23 | } 24 | } 25 | } 26 | def listOwnRepositories(option: RepositoryListOption = RepositoryListOption()): Future[List[Repository]] = 27 | doList("/user/repos", option) 28 | 29 | def listUserRepositories(user: String, option: RepositoryListOption = RepositoryListOption()): Future[List[Repository]] = 30 | doList(s"/users/$user/repos", option) 31 | 32 | def listOrgRepositories(org: String, option: RepositoryListOption = RepositoryListOption()): Future[List[Repository]] = 33 | doList(s"/orgs/$org/repos", option) 34 | 35 | def listAllPublicRepositories(since: Long = 0): Future[List[Repository]] = { 36 | val q = if (since == 0) "" else "?since=" + since 37 | exec("GET", "/repositories" + q).map { res => 38 | res.body match { 39 | case JArray(arr) => arr.map(v => Repository(v)) 40 | case _ => throw new IllegalStateException() 41 | } 42 | } 43 | } 44 | 45 | 46 | def getRepository(owner: String, repo: String): Future[Option[Repository]] = { 47 | def handleRedirect(url: String): Future[Option[Repository]] = { 48 | val regex = "https://api.github.com/repositories/(\\d+)".r 49 | url match { 50 | case regex(id) => getRepositoryById(id.toLong) 51 | case _ => Future.successful(None) 52 | } 53 | } 54 | exec("GET", s"/repos/$owner/$repo", fail404=false).flatMap { res => 55 | res.statusCode match { 56 | case 200 => Future.successful(Some(Repository(res.body))) 57 | case 301 => 58 | res.body \ "url" match { 59 | case JString(url) => handleRedirect(url) 60 | case _ => Future.successful(None) 61 | } 62 | case 404 => Future.successful(None) 63 | } 64 | } 65 | } 66 | 67 | def getRepositoryById(id: Long): Future[Option[Repository]] = { 68 | exec("GET", s"/repositories/$id", fail404=false).map { res => 69 | res.statusCode match { 70 | case 200 => Some(Repository(res.body)) 71 | case 404 => None 72 | } 73 | } 74 | } 75 | 76 | def createUserRepository(input: RepositoryInput): Future[Repository] = { 77 | exec("POST", s"/user/repos", input.value).map { res => 78 | new Repository(res.body) 79 | } 80 | } 81 | def updateRepository(input: RepositoryInput): Future[Repository] = ToDo[Future[Repository]] 82 | 83 | def removeRepository(owner: String, repo: String): Future[Boolean] = { 84 | val path = s"/repos/$owner/$repo" 85 | exec("DELETE", path).map { v => 86 | v.statusCode == 204 87 | } 88 | } 89 | 90 | def listLanguages(owner: String, repo: String): Future[LanguageList] = { 91 | val path = s"/repos/$owner/$repo/languages" 92 | exec("GET", path).map { res => 93 | LanguageList(res.body) 94 | } 95 | } 96 | 97 | /* 98 | List contributors 99 | List languages 100 | List Teams 101 | List Tags 102 | List Branches 103 | Get Branch 104 | Delete a Repository 105 | */ 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/operations/SearchOp.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.operations 2 | 3 | import scala.concurrent.Future 4 | import scala.concurrent.ExecutionContext.Implicits.global 5 | 6 | import codecheck.github.api.GitHubAPI 7 | import codecheck.github.models.SearchInput 8 | import codecheck.github.models.SearchRepositoryResult 9 | import codecheck.github.models.SearchCodeResult 10 | import codecheck.github.models.SearchIssueResult 11 | import codecheck.github.models.SearchUserResult 12 | 13 | trait SearchOp { 14 | self: GitHubAPI => 15 | 16 | def searchRepositories(input: SearchInput): Future[SearchRepositoryResult] = { 17 | val path = s"/search/repositories${input.query}" 18 | exec("GET", path ).map { res => 19 | SearchRepositoryResult(res.body) 20 | } 21 | } 22 | 23 | def searchCode(input: SearchInput): Future[SearchCodeResult] = { 24 | val path = s"/search/code${input.query}" 25 | exec("GET", path ).map { res => 26 | SearchCodeResult(res.body) 27 | } 28 | } 29 | 30 | def searchIssues(input: SearchInput): Future[SearchIssueResult] = { 31 | val path = s"/search/issues${input.query}" 32 | exec("GET", path ).map { res => 33 | SearchIssueResult(res.body) 34 | } 35 | } 36 | 37 | def searchUser(input: SearchInput): Future[SearchUserResult] = { 38 | val path = s"/search/users${input.query}" 39 | exec("GET", path ).map { res => 40 | SearchUserResult(res.body) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/operations/StatusOp.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github 2 | package operations 3 | 4 | import org.json4s.JArray 5 | 6 | import codecheck.github.models.CombinedStatus 7 | import codecheck.github.models.Status 8 | import codecheck.github.models.StatusInput 9 | 10 | import scala.concurrent.Future 11 | import scala.concurrent.ExecutionContext.Implicits.global 12 | 13 | trait StatusOp { 14 | self: api.GitHubAPI => 15 | 16 | def createStatus(owner: String, repo: String, sha: String, input: StatusInput): Future[Status] = { 17 | val path = s"/repos/$owner/$repo/statuses/$sha" 18 | exec("POST", path, input.value).map { result => 19 | Status(result.body) 20 | } 21 | } 22 | 23 | def listStatus(owner: String, repo: String, sha: String): Future[List[Status]] = { 24 | val path = s"/repos/$owner/$repo/commits/$sha/statuses" 25 | exec("GET", path, fail404 = false).map { result => 26 | result.statusCode match { 27 | case 404 => Nil 28 | case _ => result.body match { 29 | case JArray(arr) => arr.map(Status(_)) 30 | case _ => throw new IllegalStateException() 31 | } 32 | } 33 | } 34 | } 35 | 36 | def getStatus(owner: String, repo: String, sha: String): Future[CombinedStatus] = { 37 | val path = s"/repos/$owner/$repo/commits/$sha/status" 38 | exec("GET", path).map { result => 39 | CombinedStatus(result.body) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/operations/UserOp.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.operations 2 | 3 | import scala.concurrent.Future 4 | import scala.concurrent.ExecutionContext.Implicits.global 5 | import org.json4s.JArray 6 | 7 | import codecheck.github.api.GitHubAPI 8 | import codecheck.github.models.User 9 | import codecheck.github.models.UserInput 10 | import codecheck.github.utils.ToDo 11 | 12 | trait UserOp { 13 | self: GitHubAPI => 14 | 15 | def getAuthenticatedUser: Future[User] = exec("GET", "/user").map { res => 16 | User(res.body) 17 | } 18 | 19 | def getUser(username: String): Future[Option[User]] = { 20 | exec("GET", s"/users/${username}", fail404=false).map { res => 21 | res.statusCode match { 22 | case 404 => None 23 | case 200 => Some(new User(res.body)) 24 | } 25 | } 26 | } 27 | 28 | def updateAuthenticatedUser(input: UserInput): Future[User] = { 29 | exec("PATCH", s"/user",input.value,fail404=false).map{ res => 30 | User(res.body) 31 | } 32 | } 33 | 34 | def getAllUsers(since: Long = 0): Future[List[User]] = { 35 | val path = if (since == 0) "/users" else s"/users?since=$since" 36 | exec("GET", path).map ( 37 | _.body match { 38 | case JArray(arr) => arr.map(v => User(v)) 39 | case _ => throw new IllegalStateException() 40 | } 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/operations/WebhookOp.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.operations 2 | 3 | import scala.concurrent.Future 4 | import scala.concurrent.ExecutionContext.Implicits.global 5 | import org.json4s.JArray 6 | 7 | import codecheck.github.api.GitHubAPI 8 | import codecheck.github.exceptions.NotFoundException 9 | import codecheck.github.models.Webhook 10 | import codecheck.github.models.WebhookConfig 11 | import codecheck.github.models.WebhookCreateInput 12 | import codecheck.github.models.WebhookUpdateInput 13 | 14 | trait WebhookOp { 15 | self: GitHubAPI => 16 | 17 | def listWebhooks(owner: String, repo: String): Future[List[Webhook]] = { 18 | self.exec("GET", s"/repos/${owner}/${repo}/hooks").map { 19 | _.body match { 20 | case JArray(arr) => arr.map(new Webhook(_)) 21 | case _ => throw new IllegalStateException() 22 | } 23 | } 24 | } 25 | 26 | def getWebhook(owner: String, repo: String, id: Long): Future[Option[Webhook]] = { 27 | self.exec("GET", s"/repos/${owner}/${repo}/hooks/${id}", fail404=false).map { res => 28 | res.statusCode match { 29 | case 404 => None 30 | case 200 => Some(new Webhook(res.body)) 31 | } 32 | } 33 | } 34 | 35 | def createWebhook(owner: String, repo: String, input: WebhookCreateInput): Future[Webhook] = { 36 | self.exec("POST", s"/repos/${owner}/${repo}/hooks", input.value).map { res => 37 | new Webhook(res.body) 38 | } 39 | } 40 | 41 | //It is apparently an issue with Github's Webhook API that add_events and remove_events cannot be done 42 | //in a single operation. To add and remove events, it must be done through two seperate calls of updateWebhook. 43 | def updateWebhook(owner: String, repo: String, id: Long, input: WebhookUpdateInput): Future[Webhook] = { 44 | self.exec("PATCH", s"/repos/${owner}/${repo}/hooks/${id}", input.value).map { res => 45 | new Webhook(res.body) 46 | } 47 | } 48 | 49 | def testWebhook(owner: String, repo: String, id: Long): Future[Boolean] = { 50 | self.exec("POST", s"/repos/${owner}/${repo}/hooks/${id}/test", fail404=false).map { res => 51 | res.statusCode match { 52 | case 204 => true 53 | case _ => false 54 | } 55 | } 56 | } 57 | 58 | def pingWebhook(owner: String, repo: String, id: Long): Future[Boolean] = { 59 | self.exec("POST", s"/repos/${owner}/${repo}/hooks/${id}/pings", fail404=false).map { res => 60 | res.statusCode match { 61 | case 204 => true 62 | case _ => false 63 | } 64 | } 65 | } 66 | 67 | def removeWebhook(owner: String, repo: String, id: Long): Future[Boolean] = { 68 | self.exec("DELETE", s"/repos/${owner}/${repo}/hooks/${id}", fail404=false).map { res => 69 | res.statusCode match { 70 | case 204 => true 71 | case _ => false 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/transport/CompletionHandler.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.transport 2 | 3 | trait CompletionHandler { 4 | 5 | def onCompleted(res: Response): Unit 6 | def onThrowable(t: Throwable): Unit 7 | 8 | } 9 | 10 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/transport/Request.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.transport 2 | 3 | trait Request { 4 | def setBody(body: String): Request 5 | def setHeader(name: String, value: String): Request 6 | def setFollowRedirect(b: Boolean): Request 7 | def addFormParam(name: String, value: String): Request 8 | 9 | def execute(handler: CompletionHandler): Unit 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/transport/Response.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.transport 2 | 3 | trait Response { 4 | 5 | def getResponseBody: Option[String] 6 | def getStatusCode: Int 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/transport/Transport.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.transport 2 | 3 | trait Transport { 4 | 5 | def prepareGet(url: String): Request 6 | def preparePost(url: String): Request 7 | def preparePut(url: String): Request 8 | def prepareDelete(url: String): Request 9 | 10 | def close: Unit 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/transport/asynchttp19/AsyncHttp19Transport.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.transport.asynchttp19 2 | 3 | import com.ning.http.client.{AsyncHttpClient, Response => AsyncHttpResponse, AsyncCompletionHandler} 4 | 5 | import codecheck.github.transport.{Transport, Request, Response, CompletionHandler} 6 | 7 | class AsyncHttp19Transport(client: AsyncHttpClient) extends Transport{ 8 | 9 | def prepareGet(url: String): Request = new AsyncHttp19Request(client.prepareGet(url)) 10 | def preparePost(url: String): Request = new AsyncHttp19Request(client.preparePost(url)) 11 | def preparePut(url: String): Request = new AsyncHttp19Request(client.preparePut(url)) 12 | def prepareDelete(url: String): Request = new AsyncHttp19Request(client.prepareDelete(url)) 13 | 14 | def close: Unit = client.close() 15 | 16 | } 17 | 18 | class AsyncHttp19Request(request: AsyncHttpClient#BoundRequestBuilder) extends Request { 19 | 20 | def setBody(body: String): Request = { 21 | request.setBody(body) 22 | this 23 | } 24 | 25 | def setHeader(name: String, value: String): Request = { 26 | request.setHeader(name, value) 27 | this 28 | } 29 | 30 | def setFollowRedirect(b: Boolean): Request = { 31 | request.setFollowRedirects(b) 32 | this 33 | } 34 | 35 | def addFormParam(name: String, value: String): Request = { 36 | request.addFormParam(name, value) 37 | this 38 | } 39 | 40 | def execute(handler: CompletionHandler): Unit = { 41 | request.execute(new AsyncCompletionHandler[AsyncHttpResponse]() { 42 | def onCompleted(res: AsyncHttpResponse) = { 43 | handler.onCompleted(new AsyncHttp19Response(res)) 44 | res 45 | } 46 | override def onThrowable(t: Throwable): Unit = { 47 | handler.onThrowable(t) 48 | super.onThrowable(t) 49 | } 50 | }) 51 | } 52 | } 53 | 54 | class AsyncHttp19Response(response: AsyncHttpResponse) extends Response { 55 | 56 | def getResponseBody: Option[String] = Option(response.getResponseBody()) 57 | def getStatusCode: Int = response.getStatusCode 58 | } 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/transport/asynchttp20/AsyncHttp20Transport.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.transport.asynchttp20 2 | 3 | import org.asynchttpclient.{AsyncHttpClient, Response => AsyncHttpResponse, AsyncCompletionHandler, BoundRequestBuilder} 4 | 5 | import codecheck.github.transport.{Transport, Request, Response, CompletionHandler} 6 | 7 | class AsyncHttp20Transport(client: AsyncHttpClient) extends Transport{ 8 | 9 | def prepareGet(url: String): Request = new AsyncHttp20Request(client.prepareGet(url)) 10 | def preparePost(url: String): Request = new AsyncHttp20Request(client.preparePost(url)) 11 | def preparePut(url: String): Request = new AsyncHttp20Request(client.preparePut(url)) 12 | def prepareDelete(url: String): Request = new AsyncHttp20Request(client.prepareDelete(url)) 13 | 14 | def close: Unit = client.close() 15 | 16 | } 17 | 18 | class AsyncHttp20Request(request: BoundRequestBuilder) extends Request { 19 | 20 | def setBody(body: String): Request = { 21 | request.setBody(body) 22 | this 23 | } 24 | 25 | def setHeader(name: String, value: String): Request = { 26 | request.setHeader(name, value) 27 | this 28 | } 29 | 30 | def setFollowRedirect(b: Boolean): Request = { 31 | request.setFollowRedirect(b) 32 | this 33 | } 34 | 35 | def addFormParam(name: String, value: String): Request = { 36 | request.addFormParam(name, value) 37 | this 38 | } 39 | 40 | def execute(handler: CompletionHandler): Unit = { 41 | request.execute(new AsyncCompletionHandler[AsyncHttpResponse]() { 42 | def onCompleted(res: AsyncHttpResponse) = { 43 | handler.onCompleted(new AsyncHttp20Response(res)) 44 | res 45 | } 46 | override def onThrowable(t: Throwable): Unit = { 47 | handler.onThrowable(t) 48 | super.onThrowable(t) 49 | } 50 | }) 51 | } 52 | } 53 | 54 | class AsyncHttp20Response(response: AsyncHttpResponse) extends Response { 55 | 56 | def getResponseBody: Option[String] = Option(response.getResponseBody()) 57 | def getStatusCode: Int = response.getStatusCode 58 | } 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/utils/Json4s.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.utils 2 | 3 | import org.json4s._ 4 | import org.json4s.jackson.JsonMethods._ 5 | 6 | object Json4s { 7 | implicit val formats = DefaultFormats ++ org.json4s.ext.JodaTimeSerializers.all 8 | } 9 | 10 | -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/utils/PrintList.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.utils 2 | 3 | case class PrintList(headers: String*) { 4 | def format(lens: Seq[Int], row: Seq[Any]): Unit = { 5 | lens.zip(row).foreach { case (n, s) => 6 | print(s) 7 | print(" " * (n - s.toString.length)) 8 | } 9 | println 10 | } 11 | 12 | def build(items: Seq[Seq[Any]]) = { 13 | if (items.size == 0) { 14 | println("No items") 15 | } else { 16 | val lens = items.foldLeft( 17 | headers.map(_.length) 18 | ) { (ret, row) => 19 | ret.zip(row).map { case (n, s) => 20 | Math.max(n, s.toString.length) 21 | } 22 | }.map(_ + 2) 23 | 24 | format(lens, headers) 25 | println("-" * lens.sum) 26 | items.foreach(row => format(lens,row)) 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/scala/codecheck/github/utils/ToDo.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github.utils 2 | 3 | trait ToDo { 4 | throw new UnsupportedOperationException() 5 | } 6 | 7 | object ToDo { 8 | def apply[T]: T = { 9 | throw new UnsupportedOperationException() 10 | } 11 | } -------------------------------------------------------------------------------- /src/test/scala/BranchOpSpec.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github 2 | package operations 3 | 4 | import org.scalatest.FunSpec 5 | import org.scalatest.BeforeAndAfterAll 6 | import scala.concurrent.Await 7 | import scala.concurrent.ExecutionContext.Implicits.global 8 | 9 | class BranchOpSpec extends FunSpec 10 | with api.Constants 11 | with BeforeAndAfterAll 12 | { 13 | describe("getBranch") { 14 | it("with valid repo and branch should succeed") { 15 | val branchOp = Await.result(api.getBranch(user, userRepo, "master"), TIMEOUT) 16 | assert(branchOp.isDefined) 17 | assert(branchOp.get.name == "master") 18 | } 19 | it("with invalid branch should be None") { 20 | val branchOp = Await.result(api.getBranch(user, userRepo, "unknown"), TIMEOUT) 21 | assert(branchOp.isEmpty) 22 | } 23 | } 24 | 25 | describe("listBranches") { 26 | it("with valid repo should succeed") { 27 | val list = Await.result(api.listBranches(user, userRepo), TIMEOUT) 28 | assert(list.length > 0) 29 | assert(list.exists(_.name == "master")) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/scala/CollaboratorOpSpec.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github 2 | package operations 3 | 4 | import exceptions._ 5 | 6 | import org.scalatest.path.FunSpec 7 | import scala.concurrent.Await 8 | 9 | class CollaboratorOpSpec extends FunSpec with api.Constants 10 | { 11 | 12 | describe("addCollaborator"){ 13 | ignore("should add Collaborator User to user Repo"){ 14 | val res = Await.result(api.addCollaborator(user, userRepo, collaboratorUser),TIMEOUT) 15 | assert(res) 16 | } 17 | ignore("should fail for non existent User Repo"){ 18 | val res = Await.result(api.addCollaborator(user, repoInvalid, collaboratorUser).failed,TIMEOUT) 19 | res match { 20 | case e: NotFoundException => 21 | case _ => fail 22 | } 23 | } 24 | } 25 | describe("isCollaborator"){ 26 | ignore("if it is Collaborator"){ 27 | val res = Await.result(api.isCollaborator(user, userRepo, collaboratorUser),TIMEOUT) 28 | assert(res) 29 | } 30 | ignore("if it is not a valid Collaborator"){ 31 | val res1 = Await.result(api.isCollaborator(user, userRepo, otherUserInvalid),TIMEOUT) 32 | assert(res1 == false) 33 | } 34 | } 35 | describe("listCollaborators"){ 36 | ignore("should return at least one Collaborator"){ 37 | val res = Await.result(api.listCollaborators(user, userRepo),TIMEOUT) 38 | val c = res.find(_.login == collaboratorUser) 39 | assert(c.isDefined) 40 | assert(c.get.id > 0) 41 | assert(c.get.avatar_url.length > 0) 42 | assert(c.get.url.length > 0) 43 | assert(c.get.site_admin == false) 44 | } 45 | } 46 | describe("removeCollaborator"){ 47 | ignore("should remove the Collaborator"){ 48 | var res = Await.result(api.removeCollaborator(user, userRepo, collaboratorUser),TIMEOUT) 49 | assert(res == true) 50 | } 51 | } 52 | ignore("should fail for non existent User Repo"){ 53 | var res = Await.result(api.removeCollaborator(user, repoInvalid, collaboratorUser).failed,TIMEOUT) 54 | res match { 55 | case e: NotFoundException => 56 | case _ => fail 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/scala/Constants.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github 2 | package api 3 | 4 | import transport.asynchttp20.AsyncHttp20Transport 5 | 6 | import org.asynchttpclient.DefaultAsyncHttpClient 7 | import scala.concurrent.duration._ 8 | import scala.util.Random._ 9 | import org.scalatest.time.Span._ 10 | import org.scalatest.concurrent.ScalaFutures 11 | 12 | trait Constants { 13 | import scala.language.postfixOps 14 | 15 | protected val TIMEOUT = 5 seconds 16 | protected val api = Constants.API 17 | protected val shaSize = 40 18 | 19 | //Request membership of dummy organization "celestialbeing" if you are not member. Do not edit. 20 | protected val organization = "celestialbeings" 21 | protected val repo = "test-repo" 22 | 23 | //Other Options 24 | private val debug = sys.env.get("DEBUG") 25 | .map(v => v.equalsIgnoreCase("true") || v.equalsIgnoreCase("yes")).getOrElse(false) 26 | 27 | protected def showResponse(v: Any): Unit = { 28 | if (debug) { 29 | println(v) 30 | } 31 | } 32 | 33 | protected val user = sys.env("GITHUB_USER") 34 | protected val userRepo = sys.env("GITHUB_REPO") 35 | protected val userSha = "364ca6d43b5aea6a82aab5cd576eb9ccad6da537" 36 | 37 | protected val otherUser = "shunjikonishi" 38 | protected val otherUserRepo = "test-repo" 39 | protected val collaboratorUser = "shunjikonishi" 40 | protected val otherUserInvalid = "loremipsom123" 41 | protected val organizationInvalid = "loremipsom123" 42 | protected val repoInvalid = "loremipsom123" 43 | 44 | val wordBank = Array("Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta", "Theta", "Lambda", "Pi", "Sigma") 45 | def generateRandomString: String = 46 | wordBank(nextInt(10)) + " " + wordBank(nextInt(10)) + " " + wordBank(nextInt(10)) 47 | def generateRandomWord: String = wordBank(nextInt(10)) 48 | def generateRandomInt: Int = nextInt(1000) 49 | } 50 | 51 | object Constants { 52 | private val token = sys.env("GITHUB_TOKEN") 53 | implicit val client = new AsyncHttp20Transport(new DefaultAsyncHttpClient()) 54 | 55 | val API = GitHubAPI(token).withDebugHandler(new PrintlnHandler()) 56 | } 57 | -------------------------------------------------------------------------------- /src/test/scala/IssueOpSpec.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github 2 | package operations 3 | 4 | import models._ 5 | 6 | import org.scalatest.FunSpec 7 | import org.scalatest.BeforeAndAfterAll 8 | import scala.concurrent.Await 9 | import org.joda.time.DateTime 10 | import org.joda.time.DateTimeZone 11 | 12 | class IssueOpSpec extends FunSpec with api.Constants with BeforeAndAfterAll { 13 | 14 | val number = 1 15 | var nUser: Long = 0 16 | var nOrg: Long = 0 17 | var nTime: DateTime = DateTime.now 18 | val tRepo = "test-repo2" 19 | 20 | describe("createIssue(owner, repo, input)") { 21 | val input = IssueInput(Some("test issue"), Some("testing"), Some(user), None, Seq("question")) 22 | 23 | it("should create issue for user's own repo.") { 24 | val result = Await.result(api.createIssue(user, userRepo, input), TIMEOUT) 25 | nUser = result.number 26 | assert(result.url == "https://api.github.com/repos/" + user + "/" + userRepo + "/issues/" + nUser) 27 | assert(result.labels_url == "https://api.github.com/repos/" + user + "/" + userRepo + "/issues/" + nUser + "/labels{/name}") 28 | assert(result.comments_url == "https://api.github.com/repos/" + user + "/" + userRepo + "/issues/" + nUser + "/comments") 29 | assert(result.events_url == "https://api.github.com/repos/" + user + "/" + userRepo + "/issues/" + nUser + "/events") 30 | assert(result.html_url == "https://github.com/" + user + "/" + userRepo + "/issues/" + nUser) 31 | assert(result.title == "test issue") 32 | assert(result.user.login == user) 33 | assert(result.labels.head.name == "question") 34 | assert(result.state == IssueState.open) 35 | assert(result.locked == false) 36 | assert(result.assignee.get.login == user) 37 | assert(result.comments == 0) 38 | assert(result.created_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000) 39 | assert(result.updated_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000) 40 | assert(result.closed_at.isEmpty) 41 | assert(result.body == "testing") 42 | assert(result.closed_by.isEmpty) 43 | } 44 | 45 | it("should create issue for organization's repo.") { 46 | val result = Await.result(api.createIssue(organization, tRepo, input), TIMEOUT) 47 | nOrg = result.number 48 | assert(result.url == "https://api.github.com/repos/" + organization + "/" + tRepo + "/issues/" + nOrg) 49 | assert(result.labels_url == "https://api.github.com/repos/" + organization + "/" + tRepo + "/issues/" + nOrg + "/labels{/name}") 50 | assert(result.comments_url == "https://api.github.com/repos/" + organization + "/" + tRepo+ "/issues/" + nOrg + "/comments") 51 | assert(result.events_url == "https://api.github.com/repos/" + organization + "/" + tRepo + "/issues/" + nOrg + "/events") 52 | assert(result.html_url == "https://github.com/" + organization + "/" + tRepo + "/issues/" + nOrg) 53 | assert(result.title == "test issue") 54 | assert(result.user.login == user) 55 | assert(result.labels.isEmpty) //Label is not set if user is not the organization member. 56 | assert(result.state == IssueState.open) 57 | assert(result.locked == false) 58 | assert(result.assignee.isEmpty) //Assignee is not set if user is not the organization member. 59 | assert(result.milestone.isEmpty) //Assignee is not set if user is not the organization member. 60 | assert(result.comments == 0) 61 | assert(result.created_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000) 62 | assert(result.updated_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000) 63 | assert(result.body == "testing") 64 | assert(result.closed_by.isEmpty) 65 | } 66 | } 67 | 68 | describe("getIssue(owner, repo, number)") { 69 | it("should return issue from user's own repo.") { 70 | val result = Await.result(api.getIssue(user, userRepo, nUser), TIMEOUT) 71 | assert(result.get.title == "test issue") 72 | } 73 | 74 | it("should return issue from organization's repo.") { 75 | val result = Await.result(api.getIssue(organization, tRepo, nOrg), TIMEOUT) 76 | assert(result.get.title == "test issue") 77 | } 78 | } 79 | 80 | describe("unassign(owner, repo, number)") { 81 | it("should succeed with valid inputs on issues in user's own repo.") { 82 | val result = Await.result(api.unassign(user, userRepo, nUser), TIMEOUT) 83 | assert(result.opt("assignee").isEmpty) 84 | } 85 | 86 | // it("should succeed with valid inputs on issues in organization's repo.") { 87 | // val result = Await.result(api.unassign(organization, tRepo, nOrg), TIMEOUT) 88 | // assert(result.opt("assignee").isEmpty) 89 | // } 90 | } 91 | 92 | describe("assign(owner, repo, number, assignee)") { 93 | it("should succeed with valid inputs on issues in user's own repo.") { 94 | val result = Await.result(api.assign(user, userRepo, nUser, user), TIMEOUT) 95 | assert(result.get("assignee.login") == user) 96 | } 97 | 98 | // it("should succeed with valid inputs on issues in organization's repo.") { 99 | // val result = Await.result(api.assign(organization, tRepo, nOrg, user), TIMEOUT) 100 | // assert(result.get("assignee.login") == user) 101 | // } 102 | } 103 | 104 | describe("listAllIssues(option)") { 105 | it("shold return at least one issue.") { 106 | val result = Await.result(api.listAllIssues(), TIMEOUT) 107 | assert(result.length > 0) 108 | } 109 | 110 | it("shold return only two issues when using options.") { 111 | val option = IssueListOption(IssueFilter.created, IssueState.open, Seq("question"), since=Some(nTime)) 112 | val result = Await.result(api.listAllIssues(option), TIMEOUT) 113 | assert(result.length > 0) 114 | assert(result.head.title == "test issue") 115 | } 116 | } 117 | 118 | describe("listUserIssues(option)") { 119 | it("shold return at least one issue.") { 120 | val result = Await.result(api.listUserIssues(), TIMEOUT) 121 | assert(result.length > 0) 122 | } 123 | 124 | it("shold return only one issues when using options.") { 125 | val option = IssueListOption(IssueFilter.created, IssueState.open, Seq("question"), since=Some(nTime)) 126 | val result = Await.result(api.listUserIssues(option), TIMEOUT) 127 | assert(result.length > 0) 128 | assert(result.head.title == "test issue") 129 | } 130 | } 131 | 132 | describe("listRepositoryIssues(owner, repo, option)") { 133 | it("should return at least one issue from user's own repo.") { 134 | val result = Await.result(api.listRepositoryIssues(user, userRepo), TIMEOUT) 135 | assert(result.length > 0) 136 | } 137 | 138 | it("should return at least one issue from organization's repo.") { 139 | val result = Await.result(api.listRepositoryIssues(organization, tRepo), TIMEOUT) 140 | assert(result.length > 0) 141 | } 142 | 143 | it("should return only one issue from user's own repo when using options.") { 144 | val option = new IssueListOption4Repository(Some(MilestoneSearchOption(1)), IssueState.open, Some(user), Some(user), labels=Seq("question"), since=Some(nTime)) 145 | val result = Await.result(api.listRepositoryIssues(user, userRepo, option), TIMEOUT) 146 | //showResponse(option.q) 147 | assert(result.length == 1) 148 | assert(result.head.title == "test issue") 149 | } 150 | 151 | it("should return only one issue from organization's repo when using options.") { 152 | val option = new IssueListOption4Repository(None, IssueState.open, None, Some(user), labels=Nil, since=Some(nTime)) 153 | val result = Await.result(api.listRepositoryIssues(organization, tRepo, option), TIMEOUT) 154 | assert(result.length == 1) 155 | assert(result.head.title == "test issue") 156 | } 157 | } 158 | 159 | describe("editIssue(owner, repo, number, input)") { 160 | val input = IssueInput(Some("test issue edited"), Some("testing again"), Some(user), None, Seq("question", "bug"), Some(IssueState.closed)) 161 | 162 | it("should edit the issue in user's own repo.") { 163 | val result = Await.result(api.editIssue(user, userRepo, nUser, input), TIMEOUT) 164 | assert(result.title == "test issue edited") 165 | assert(result.body == "testing again") 166 | assert(result.labels.head.name == "bug") 167 | assert(result.state == IssueState.closed) 168 | assert(result.updated_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000) 169 | } 170 | 171 | it("should edit the issue in organization's repo.") { 172 | val result = Await.result(api.editIssue(organization, tRepo, nOrg, input), TIMEOUT) 173 | assert(result.title == "test issue edited") 174 | assert(result.body == "testing again") 175 | assert(result.milestone.isEmpty) 176 | assert(result.labels.isEmpty) 177 | assert(result.state == IssueState.closed) 178 | assert(result.updated_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000) 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/test/scala/LabelOpSpec.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github 2 | package operations 3 | 4 | import exceptions._ 5 | import models._ 6 | 7 | import org.scalatest.FunSpec 8 | import scala.concurrent.Await 9 | 10 | class LabelOpSpec extends FunSpec with api.Constants { 11 | 12 | val number = 1 13 | val gName = generateRandomWord 14 | 15 | describe("removeAllLabels") { 16 | it("should succeed") { 17 | val result = Await.result(api.removeAllLabels(user, userRepo, number), TIMEOUT) 18 | assert(result.length == 0) 19 | } 20 | } 21 | 22 | describe("addLabel") { 23 | it("should succeed") { 24 | val result = Await.result(api.addLabels(user, userRepo, number, "bug"), TIMEOUT) 25 | assert(result.length == 1) 26 | val label = result.head 27 | assert(label.name == "bug") 28 | assert(label.url.length > 0) 29 | assert(label.color.length == 6) 30 | } 31 | } 32 | 33 | describe("replaceLabels") { 34 | it("should succeed") { 35 | val result = Await.result(api.replaceLabels(user, userRepo, number, "duplicate", "invalid"), TIMEOUT) 36 | assert(result.length == 2) 37 | assert(result.filter(_.name == "duplicate").length == 1) 38 | assert(result.filter(_.name == "invalid").length == 1) 39 | } 40 | } 41 | 42 | describe("removeLabel") { 43 | it("should succeed") { 44 | val result = Await.result(api.removeLabel(user, userRepo, number, "duplicate"), TIMEOUT) 45 | assert(result.length == 1) 46 | 47 | val result2 = Await.result(api.removeLabel(user, userRepo, number, "invalid"), TIMEOUT) 48 | assert(result2.length == 0) 49 | } 50 | } 51 | 52 | describe("listLabels") { 53 | it("listLabels should succeed") { 54 | Await.result(api.addLabels(user, userRepo, number, "invalid"), TIMEOUT) 55 | val result = Await.result(api.listLabels(user, userRepo, number), TIMEOUT) 56 | assert(result.length == 1) 57 | val label = result.head 58 | assert(label.name == "invalid") 59 | assert(label.url.length > 0) 60 | assert(label.color.length == 6) 61 | } 62 | } 63 | 64 | describe("listLabelDefs") { 65 | it("should succeed") { 66 | val result = Await.result(api.listLabelDefs(user, userRepo), TIMEOUT) 67 | assert(result.length > 0) 68 | val label = result.head 69 | assert(label.name.length > 0) 70 | assert(label.url.length > 0) 71 | assert(label.color.length == 6) 72 | } 73 | } 74 | 75 | describe("getLabelDef") { 76 | it("should succeed") { 77 | Await.result(api.getLabelDef(user, userRepo, "question"), TIMEOUT).map { label => 78 | assert(label.name == "question") 79 | assert(label.url.length > 0) 80 | assert(label.color == "cc317c") 81 | } 82 | } 83 | it("should be None") { 84 | assert(Await.result(api.getLabelDef(user, userRepo, "hoge"), TIMEOUT).isEmpty) 85 | } 86 | } 87 | 88 | describe("createLabelDef") { 89 | it("should succeed") { 90 | val input = LabelInput(gName, "cc317c") 91 | val label = Await.result(api.createLabelDef(user, userRepo, input), TIMEOUT) 92 | assert(label.name == gName) 93 | assert(label.url.length > 0) 94 | assert(label.color == "cc317c") 95 | } 96 | it("again should fail") { 97 | val input = LabelInput(gName, "cc317c") 98 | try { 99 | val label = Await.result(api.createLabelDef(user, userRepo, input), TIMEOUT) 100 | fail 101 | } catch { 102 | case e: GitHubAPIException => 103 | assert(e.error.errors.length == 1) 104 | assert(e.error.errors.head.code == "already_exists") 105 | } 106 | } 107 | } 108 | 109 | describe("updateLabelDef") { 110 | it("should succeed") { 111 | val input = LabelInput(gName, "84b6eb") 112 | val label = Await.result(api.updateLabelDef(user, userRepo, gName, input), TIMEOUT) 113 | assert(label.name == gName) 114 | assert(label.url.length > 0) 115 | assert(label.color == "84b6eb") 116 | } 117 | } 118 | 119 | describe("removeLabelDef") { 120 | it("should succeed") { 121 | val result = Await.result(api.removeLabelDef(user, userRepo, gName), TIMEOUT) 122 | assert(result) 123 | } 124 | it("removeLabelDef again should fail") { 125 | try { 126 | val result = Await.result(api.removeLabelDef(user, userRepo, gName), TIMEOUT) 127 | fail 128 | } catch { 129 | case e: NotFoundException => 130 | assert(e.error.errors.length == 0) 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/test/scala/MilestoneOpSpec.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github 2 | package operations 3 | 4 | import exceptions._ 5 | import models._ 6 | 7 | import org.scalatest.path.FunSpec 8 | import scala.concurrent.Await 9 | import scala.concurrent.ExecutionContext.Implicits.global 10 | import org.joda.time.DateTime 11 | 12 | class MilestoneOpSpec extends FunSpec 13 | with api.Constants 14 | { 15 | 16 | private def removeAll = { 17 | val list = Await.result(api.listMilestones(user, userRepo, MilestoneListOption(state=MilestoneState.all)), TIMEOUT) 18 | list.foreach { m => 19 | Await.result(api.removeMilestone(user, userRepo, m.number), TIMEOUT) 20 | } 21 | } 22 | private def create(input: MilestoneInput): Milestone = { 23 | Await.result(api.createMilestone(user, userRepo, input), TIMEOUT) 24 | } 25 | 26 | describe("createMilestone") { 27 | removeAll 28 | val gName = generateRandomString 29 | val gDescription = generateRandomString 30 | val d1 = DateTime.now().plusMonths(1).withMillisOfSecond(0) 31 | 32 | it("without description and due_on should succeed") { 33 | val input = MilestoneInput(gName) 34 | val m = Await.result(api.createMilestone(user, userRepo, input), TIMEOUT) 35 | assert(m.title == gName) 36 | assert(m.state == MilestoneState.open) 37 | assert(m.description.isEmpty) 38 | assert(m.due_on.isEmpty) 39 | } 40 | it("without due_on should succeed") { 41 | val input = MilestoneInput(gName, gDescription) 42 | val m = Await.result(api.createMilestone(user, userRepo, input), TIMEOUT) 43 | assert(m.title == gName) 44 | assert(m.state == MilestoneState.open) 45 | assert(m.description.get == gDescription) 46 | assert(m.due_on.isEmpty) 47 | } 48 | it("without description should succeed") { 49 | val input = MilestoneInput(gName, d1) 50 | val m = Await.result(api.createMilestone(user, userRepo, input), TIMEOUT) 51 | assert(m.title == gName) 52 | assert(m.state == MilestoneState.open) 53 | assert(m.description.isEmpty) 54 | // assert(m.due_on.get == d1) 55 | } 56 | it("with description and due_on should succeed") { 57 | val input = MilestoneInput(gName, gDescription, d1) 58 | val m = Await.result(api.createMilestone(user, userRepo, input), TIMEOUT) 59 | assert(m.title == gName) 60 | assert(m.state == MilestoneState.open) 61 | assert(m.description.get == gDescription) 62 | // assert(m.due_on.get == d1) 63 | } 64 | it("with wrong reponame should fail") { 65 | val input = MilestoneInput(gName, gDescription, d1) 66 | val ex = Await.result(api.createMilestone(user, repoInvalid, input).failed, TIMEOUT) 67 | ex match { 68 | case e: NotFoundException => 69 | case _ => fail 70 | } 71 | } 72 | } 73 | describe("getMilestone") { 74 | removeAll 75 | val gName = generateRandomString 76 | val gDescription = generateRandomString 77 | val d1 = DateTime.now().plusMonths(1).withMillisOfSecond(0) 78 | val m1 = create(MilestoneInput(gName, gDescription, d1)) 79 | 80 | it("should succeed") { 81 | Await.result(api.getMilestone(user, userRepo, m1.number), TIMEOUT).map { m => 82 | assert(m.url == m1.url) 83 | assert(m.id == m1.id) 84 | assert(m.number == m1.number) 85 | assert(m.state == MilestoneState.open) 86 | assert(m.title == gName) 87 | assert(m.description.get == gDescription) 88 | assert(m.creator.login == m1.creator.login) 89 | assert(m.open_issues == 0) 90 | assert(m.closed_issues == 0) 91 | assert(m.created_at != null) 92 | assert(m.updated_at != null) 93 | assert(m.closed_at.isEmpty) 94 | // assert(m.due_on.get == d1) 95 | } 96 | } 97 | it("should be None") { 98 | assert(Await.result(api.getMilestone(user, userRepo, 999), TIMEOUT).isEmpty) 99 | } 100 | } 101 | describe("updateMilestone") { 102 | val gName1 = generateRandomString 103 | val gDescription1 = generateRandomString 104 | val d1 = DateTime.now().plusMonths(1).withMillisOfSecond(0) 105 | val gName2 = generateRandomString 106 | val gDescription2 = generateRandomString 107 | val d2 = d1.plusMonths(1) 108 | 109 | removeAll 110 | val m1 = create(MilestoneInput(gName1, gDescription1, d1)) 111 | 112 | it("should succeed") { 113 | val input = MilestoneInput( 114 | title=Some(gName2), 115 | state=Some(MilestoneState.closed), 116 | description=Some(gDescription2), 117 | due_on=Some(d2) 118 | ) 119 | val m = Await.result(api.updateMilestone(user, userRepo, m1.number, input), TIMEOUT) 120 | assert(m.id == m1.id) 121 | assert(m.number == m1.number) 122 | assert(m.state == MilestoneState.closed) 123 | assert(m.title == gName2) 124 | assert(m.description.get == gDescription2) 125 | assert(m.closed_at.isDefined) 126 | // assert(m.due_on.get == d2) 127 | 128 | Await.result(api.getMilestone(user, userRepo, m.number), TIMEOUT).map { m2=> 129 | assert(m2.id == m1.id) 130 | assert(m2.number == m1.number) 131 | assert(m2.state == MilestoneState.closed) 132 | assert(m2.title == gName2) 133 | assert(m2.description.get == gDescription2) 134 | assert(m2.closed_at.isDefined) 135 | // assert(m2.due_on.get == d2) 136 | } 137 | } 138 | } 139 | describe("listMilestones") { 140 | val gName1 = generateRandomString 141 | val gDescription1 = generateRandomString 142 | val d1 = DateTime.now().plusMonths(1).withMillisOfSecond(0) 143 | val gName2 = generateRandomString 144 | val gDescription2 = generateRandomString 145 | val d2 = d1.plusMonths(1) 146 | 147 | removeAll 148 | val m1 = create(MilestoneInput(gName1, gDescription1, d1)) 149 | val m2 = create(MilestoneInput(gName2, gDescription2, d2)) 150 | 151 | it("should succeed") { 152 | val list = Await.result(api.listMilestones(user, userRepo), TIMEOUT) 153 | assert(list.size == 2) 154 | val m = list.head 155 | assert(m.title == gName1) 156 | // assert(m.due_on.get == d1) 157 | } 158 | it("with sort desc should succeed") { 159 | val option = MilestoneListOption(direction=SortDirection.desc) 160 | val list = Await.result(api.listMilestones(user, userRepo, option), TIMEOUT) 161 | assert(list.size == 2) 162 | val m = list.head 163 | assert(m.title == gName2) 164 | // assert(m.due_on.get == d2) 165 | } 166 | it("with wrong reponame should fail") { 167 | val ex = Await.result(api.listMilestones(user, repoInvalid).failed, TIMEOUT) 168 | ex match { 169 | case e: NotFoundException => 170 | case _ => fail 171 | } 172 | } 173 | } 174 | describe("removeMilestone") { 175 | val gName = generateRandomString 176 | val gDescription = generateRandomString 177 | val d1 = DateTime.now().plusMonths(1).withMillisOfSecond(0) 178 | 179 | removeAll 180 | val m1 = create(MilestoneInput(gName, gDescription, d1)) 181 | 182 | it("should succeed") { 183 | val b = Await.result(api.removeMilestone(user, userRepo, m1.number), TIMEOUT) 184 | assert(b) 185 | 186 | assert(Await.result(api.getMilestone(user, userRepo, m1.number), TIMEOUT).isEmpty) 187 | 188 | val ex = Await.result(api.removeMilestone(user, userRepo, m1.number).failed, TIMEOUT) 189 | ex match { 190 | case e: NotFoundException => 191 | case _ => fail 192 | } 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/test/scala/OraganizationOpSpec.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github 2 | package operations 3 | 4 | import org.scalatest.FunSpec 5 | import org.scalatest.BeforeAndAfter 6 | import scala.concurrent.Await 7 | import org.joda.time.DateTime 8 | import org.joda.time.DateTimeZone 9 | 10 | import codecheck.github.models.OrganizationInput 11 | 12 | class OrganizationOpSpec extends FunSpec with api.Constants with BeforeAndAfter { 13 | 14 | describe("listOwnOrganizations") { 15 | it("should return result.") { 16 | val result = Await.result(api.listOwnOrganizations, TIMEOUT) 17 | assert(result.length >= 0) 18 | } 19 | } 20 | 21 | describe("listUserOrganizations(user)") { 22 | it("should return at least one organization.") { 23 | val result = Await.result(api.listUserOrganizations(otherUser), TIMEOUT) 24 | assert(result.length >= 1) 25 | } 26 | 27 | // it("should return multiple organizations if user belongs in more than one.") { 28 | // val result = Await.result(api.listUserOrganizations(otherUser), TIMEOUT) 29 | // println(result) 30 | // assert(result.length > 1) 31 | // } 32 | } 33 | 34 | describe("getOrganization") { 35 | it("should return correct values.") { 36 | Await.result(api.getOrganization(organization), TIMEOUT).map { org => 37 | showResponse(org) 38 | assert(org.login == "celestialbeings") 39 | assert(org.id > 0) 40 | assert(org.url == "https://api.github.com/orgs/celestialbeings") 41 | assert(org.repos_url == "https://api.github.com/orgs/celestialbeings/repos") 42 | assert(org.events_url == "https://api.github.com/orgs/celestialbeings/events") 43 | assert(org.members_url == "https://api.github.com/orgs/celestialbeings/members{/member}") 44 | assert(org.public_members_url == "https://api.github.com/orgs/celestialbeings/public_members{/member}") 45 | assert(org.avatar_url.length > 0) 46 | assert(org.description == "No description") 47 | assert(org.name == "celestialbeings") 48 | assert(org.company.get == "givery") 49 | assert(org.blog == "") 50 | assert(org.location == "Tokyo") 51 | assert(org.email == "") 52 | assert(org.public_repos == 2) 53 | assert(org.public_gists == 0) 54 | assert(org.followers == 0) 55 | assert(org.following == 0) 56 | assert(org.html_url == "https://github.com/celestialbeings") 57 | assert(org.created_at.toDateTime(DateTimeZone.UTC) == DateTime.parse("2015-05-13T06:28:23Z").toDateTime(DateTimeZone.UTC)) 58 | assert(org.updated_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000) 59 | assert(org.`type` == "Organization") 60 | } 61 | } 62 | 63 | it("should return None if invalid organization.") { 64 | assert(Await.result(api.getOrganization(organizationInvalid), TIMEOUT).isEmpty) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/scala/PullRequestOpSpec.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github 2 | package operations 3 | 4 | import models._ 5 | 6 | import org.scalatest.FunSpec 7 | import scala.concurrent.Await 8 | import java.util.Date 9 | 10 | class PullRequestOpSpec extends FunSpec with api.Constants { 11 | 12 | describe("listPullRequests") { 13 | it("with valid repo should succeed") { 14 | val list = Await.result(api.listPullRequests(otherUser, otherUserRepo), TIMEOUT) 15 | assert(list.length >= 0) 16 | assert(list.exists(_.state == IssueState.open)) 17 | assert(list.exists(_.mergeable == None)) 18 | assert(list.exists(_.merged == None)) 19 | assert(list.exists(_.merge_commit_sha.size == shaSize)) 20 | assert(list.exists(_.merged_by == None)) 21 | assert(list.exists(_.comments == None)) 22 | assert(list.exists(_.commits == None)) 23 | assert(list.exists(_.additions == None)) 24 | assert(list.exists(_.deletions == None)) 25 | assert(list.exists(_.changed_files == None)) 26 | assert(list.exists(_.maintainer_can_modify == None)) 27 | assert(list.exists(_.base.repo.exists(_.full_name == s"$otherUser/$otherUserRepo"))) 28 | assert(list.exists(_.base.user.login == otherUser)) 29 | assert(list.exists(_.base.repo.exists(_.name == otherUserRepo))) 30 | } 31 | } 32 | 33 | describe("getPullRequest") { 34 | it("with open PR should succeed") { 35 | val pr = Await.result(api.getPullRequest(otherUser, otherUserRepo, 21L), TIMEOUT) 36 | assert(pr.nonEmpty) 37 | assert(pr.exists(_.state == IssueState.closed)) 38 | assert(pr.exists(_.mergeable == Some(false))) 39 | assert(pr.exists(_.merge_commit_sha.size == shaSize)) 40 | assert(pr.exists(_.merged_by == None)) 41 | assert(pr.exists(_.comments.exists(_ >= 0))) 42 | assert(pr.exists(_.commits.exists(_ >= 0))) 43 | assert(pr.exists(_.additions.exists(_ >= 0))) 44 | assert(pr.exists(_.deletions.exists(_ >= 0))) 45 | assert(pr.exists(_.changed_files.exists(_ >= 0))) 46 | assert(pr.exists(_.maintainer_can_modify == Some(false))) 47 | } 48 | } 49 | 50 | describe("createPullRequest(owner, repo, input)") { 51 | val username = otherUser 52 | val reponame = otherUserRepo 53 | 54 | // NOTE: Can only create pull requests for submitting user 55 | it("should success create and close") { 56 | val title = "Test Pull Request " + new Date().toString() 57 | val input = PullRequestInput(title, "githubapi-test-pr", "master", Some("PullRequest body")) 58 | val result = Await.result(api.createPullRequest(username, reponame, input), TIMEOUT) 59 | assert(result.title == title) 60 | assert(result.state == IssueState.open) 61 | 62 | val result2 = Await.result(api.closePullRequest(username, reponame, result.number), TIMEOUT) 63 | assert(result2.title == title) 64 | assert(result2.state == IssueState.closed) 65 | } 66 | 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/test/scala/PullRequestReviewOpSpec.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github 2 | package operations 3 | 4 | import models._ 5 | 6 | import org.scalatest.FunSpec 7 | import scala.concurrent.Await 8 | import java.util.Date 9 | 10 | class PullRequestReviewOpSpec extends FunSpec with api.Constants { 11 | 12 | describe("listPullRequestReviews") { 13 | it("with valid repo should succeed") { 14 | val list = Await.result(api.listPullRequestReviews(otherUser, otherUserRepo, 47), TIMEOUT) 15 | assert(list.length >= 0) 16 | assert(list.exists(_.id >= 0)) 17 | assert(list.exists(_.state == PullRequestReviewState.approved)) 18 | assert(list.exists(_.commit_id.size == shaSize)) 19 | } 20 | } 21 | 22 | describe("getPullRequestReview") { 23 | it("with valid repo should succeed") { 24 | val review = Await.result(api.getPullRequestReview(otherUser, otherUserRepo, 47, 32477105), TIMEOUT) 25 | assert(review.nonEmpty) 26 | assert(review.exists(_.id >= 0)) 27 | assert(review.exists(_.state == PullRequestReviewState.approved)) 28 | assert(review.exists(_.commit_id.size == shaSize)) 29 | } 30 | } 31 | 32 | describe("createPullRequestReview(owner, repo, number, input)") { 33 | val username = otherUser 34 | val reponame = otherUserRepo 35 | 36 | it("should success create and close") { 37 | val body = "Test PR review " + new Date().toString() 38 | val input = PullRequestReviewInput( 39 | Some(body), 40 | Some(PullRequestReviewStateInput.REQUEST_CHANGES), 41 | Seq( 42 | PullRequestReviewCommentInput( 43 | "challenge.json", 44 | 1L, 45 | "Comment body" 46 | ) 47 | ) 48 | ) 49 | 50 | // NOTE: You can only add reviews to PRs that aren't your own 51 | val result = Await.result(api.createPullRequestReview(username, reponame, 47, input), TIMEOUT) 52 | assert(result.body == body) 53 | assert(result.state == PullRequestReviewState.changes_requested) 54 | 55 | // NOTE: You can only dismiss reviews on repos you have rights 56 | // val result2 = Await.result(api.dismissPullRequestReview(username, reponame, 47, result.id, "githubapi-test-pr-review"), TIMEOUT) 57 | // assert(result.body == Some(body)) 58 | // assert(result.state == PullRequestReviewState.dismissed) 59 | } 60 | 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/test/scala/RepositoryAPISpec.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github 2 | package api 3 | 4 | import org.scalatest.FunSpec 5 | 6 | class RepositoryAPISpec extends FunSpec with api.Constants { 7 | 8 | val gDummy = generateRandomString 9 | val gRepo = generateRandomString 10 | 11 | describe("with dummy repo") { 12 | val repo = api.repositoryAPI(gDummy, gRepo) 13 | it(s"should has owner ${gDummy}") { 14 | assert(repo.owner == gDummy) 15 | } 16 | it(s"should has repo ${gRepo}") { 17 | assert(repo.repo == gRepo) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/scala/RepositoryOpSpec.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github 2 | package operations 3 | 4 | import org.scalatest.path.FunSpec 5 | import scala.concurrent.Await 6 | import scala.concurrent.ExecutionContext.Implicits.global 7 | 8 | class RepositoryOpSpec extends FunSpec with api.Constants 9 | { 10 | 11 | describe("listOwnRepositories") { 12 | it("should succeed") { 13 | val list = Await.result(api.listOwnRepositories(), TIMEOUT) 14 | assert(list.size > 0) 15 | } 16 | 17 | it("Response: listOwnRepositories()") { 18 | val list = Await.result(api.listOwnRepositories(), TIMEOUT) 19 | showResponse(list) 20 | } 21 | //ToDo option test 22 | } 23 | describe("listUserRepositories") { 24 | it("should succeed") { 25 | val list = Await.result(api.listUserRepositories(otherUser), TIMEOUT) 26 | assert(list.size > 0) 27 | } 28 | 29 | it("Response: listUserRepositories()") { 30 | val list = Await.result(api.listUserRepositories(otherUser), TIMEOUT) 31 | showResponse(list) 32 | assert(list.size > 0) 33 | } 34 | } 35 | describe("listOrgRepositories") { 36 | it("should succeed with valid organization.") { 37 | val list = Await.result(api.listOrgRepositories(organization), TIMEOUT) 38 | assert(list.size > 0) 39 | } 40 | 41 | it("Response: listOrgRepositories()") { 42 | val list = Await.result(api.listOrgRepositories(organization), TIMEOUT) 43 | showResponse(list) 44 | assert(list.size > 0) 45 | } 46 | 47 | 48 | } 49 | describe("getRepository") { 50 | it("should succeed") { 51 | Await.result(api.getRepository(organization, repo), TIMEOUT).map { res => 52 | assert(res.owner.login == organization) 53 | assert(res.name == repo) 54 | assert(res.full_name == organization + "/" + repo) 55 | assert(res.url == "https://api.github.com/repos/" + organization + "/" + repo) 56 | //ToDo add more fields 57 | } 58 | } 59 | it("should be None") { 60 | assert(Await.result(api.getRepository(organization, repoInvalid), TIMEOUT).isEmpty) 61 | } 62 | } 63 | 64 | describe("listLanguages") { 65 | it("should succeed") { 66 | val username = "shunjikonishi" 67 | val reponame = "programming-game" 68 | val list = Await.result(api.listLanguages(username, reponame), TIMEOUT) 69 | assert(list.items.size > 0) 70 | val sumRate = list.items.map(_.rate).sum 71 | assert(sumRate > 0.99 && sumRate <= 1.0) 72 | } 73 | } 74 | 75 | /* 76 | describe("createUserRepository") { 77 | val createRepoName = "create-repo-name" 78 | it("should succeed") { 79 | val input = RepositoryInput(name = createRepoName) 80 | val repo = Await.result(api.createUserRepository(input), TIMEOUT) 81 | assert(repo.owner.login == user) 82 | assert(repo.name == "create-repo-test") 83 | } 84 | it("should fail with existing repository name") { 85 | val input = RepositoryInput(name = createRepoName) 86 | try { 87 | val repo = Await.result(api.createUserRepository(input), TIMEOUT) 88 | fail 89 | } catch { 90 | case e: ApiException => 91 | assert(e.error.errors.head.field == "name") 92 | case e: Throwable => 93 | fail 94 | } 95 | } 96 | } 97 | */ 98 | } 99 | -------------------------------------------------------------------------------- /src/test/scala/SearchOpSpec.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github 2 | package operations 3 | 4 | import models._ 5 | 6 | import org.scalatest.path.FunSpec 7 | import scala.concurrent.Await 8 | import scala.concurrent.ExecutionContext.Implicits.global 9 | import codecheck.github.exceptions.GitHubAPIException 10 | 11 | class SearchOpSpec extends FunSpec 12 | with api.Constants 13 | { 14 | 15 | describe("searchRepositories") { 16 | it("with valid SearchInput should succeed") { 17 | val q = "tetris language:assembly".trim.replaceAll(" ","+") 18 | val input = SearchRepositoryInput(q,sort=Some(SearchRepositorySort.stars),order=SortDirection.desc) 19 | val res = Await.result(api.searchRepositories(input), TIMEOUT) 20 | assert(res.total_count >= 1) 21 | assert(res.items(0).id >= 1 ) 22 | assert(res.items(0).name.length >= 1) 23 | assert(res.items(0).full_name.length >= 1) 24 | assert(res.items(0).description.isDefined) 25 | assert(res.items(0).open_issues_count >= 0) 26 | assert(res.items(0).language == "Assembly") 27 | assert(res.items(0).stargazers_count > res.items(1).stargazers_count) 28 | } 29 | it("with valid changed query(q) SearchInput should succeed") { 30 | val q = "jquery in:name,description".trim.replaceAll(" ","+") 31 | val input = SearchRepositoryInput(q,sort=Some(SearchRepositorySort.stars),order=SortDirection.desc) 32 | val res = Await.result(api.searchRepositories(input), TIMEOUT) 33 | assert(res.total_count >= 1) 34 | assert(res.items(0).id >= 1 ) 35 | assert(res.items(0).name.length >= 1) 36 | assert(res.items(0).full_name.length >= 1) 37 | assert(res.items(0).description.isDefined) 38 | assert(res.items(0).open_issues_count >= 0) 39 | } 40 | } 41 | describe("searchCode") { 42 | it("with valid SearchInput q,no SortOrder should succeed") { 43 | val q = "addClass in:file language:js repo:jquery/jquery".trim.replaceAll(" ","+") 44 | val input = SearchCodeInput(q,sort=None,order=SortDirection.desc) 45 | val res = Await.result(api.searchCode(input), TIMEOUT) 46 | assert(res.total_count >= 1) 47 | assert(res.items(0).repository.id >= 1 ) 48 | assert(res.items(0).sha.length >= 40) 49 | assert(res.items(0).score >= 0d) 50 | assert(res.items(0).repository.full_name == "jquery/jquery") 51 | } 52 | it("with valid SearchInput it should succeed") { 53 | val q = "function size:10000 language:python".trim.replaceAll(" ","+") 54 | val input = SearchCodeInput(q,sort=Some(SearchCodeSort.indexed),order=SortDirection.asc) 55 | val res = Await.result(api.searchCode(input), TIMEOUT) 56 | assert(res.total_count >= 1) 57 | assert(res.items(0).repository.id >= 1 ) 58 | assert(res.items(0).path.endsWith(".py")) 59 | assert(res.items(0).sha.length >= 40) 60 | assert(res.items(0).score >= 0d) 61 | assert(res.items(0).repository.`private` == false) 62 | } 63 | } 64 | describe("searchIssues") { 65 | it("with valid SearchInput should succeed") { 66 | val q = "windows label:bug language:python state:open".trim.replaceAll(" ","+") 67 | val input = SearchIssueInput(q,sort=Some(SearchIssueSort.created),order=SortDirection.desc) 68 | val res = Await.result(api.searchIssues(input), TIMEOUT) 69 | assert(res.total_count >= 1) 70 | assert(res.items(0).labels(0).name.toLowerCase == "bug" ) 71 | assert(res.items(0).state == IssueState.open) 72 | assert(((res.items(0).created_at).compareTo(res.items(1).created_at)) > 0) 73 | } 74 | } 75 | describe("searchUser") { 76 | it("with valid SearchInput should succeed") { 77 | val q = "tom repos:>42 followers:>1000" 78 | .trim.replaceAll(" ","+") 79 | .replaceAll(">","%3E") 80 | val input = SearchUserInput(q,sort=None,order=SortDirection.desc) 81 | val res = Await.result(api.searchUser(input), TIMEOUT) 82 | assert(res.total_count >= 0) 83 | assert(res.items(0).login.length >= 0) 84 | assert(res.items(0).id >= 0) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/scala/StatusOpSpec.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github 2 | package operations 3 | 4 | import models._ 5 | 6 | import codecheck.github.models.Status 7 | import codecheck.github.models.StatusInput 8 | import codecheck.github.models.StatusState 9 | 10 | import exceptions._ 11 | 12 | import codecheck.github.exceptions.GitHubAPIException 13 | import codecheck.github.exceptions.NotFoundException 14 | 15 | import org.scalatest.FunSpec 16 | import scala.concurrent.Await 17 | 18 | class StatusOpSpec extends FunSpec with api.Constants { 19 | 20 | describe("listStatus(owner, repo, sha)") { 21 | 22 | it("should have zero or more statuses") { 23 | val result = Await.result(api.listStatus(user, userRepo, userSha), TIMEOUT) 24 | result.map { status => 25 | assert(StatusState.values.contains(status.state)) 26 | } 27 | } 28 | } 29 | 30 | describe("getStatus(owner, repo, sha)") { 31 | 32 | it("should have a status or not") { 33 | val result = Await.result(api.getStatus(user, userRepo, userSha), TIMEOUT) 34 | assert(StatusState.values.contains(result.state)) 35 | assert(result.sha == userSha) 36 | assert(result.total_count >= 0L) 37 | assert(result.statuses.length >= 0L) 38 | result.statuses.map { status => 39 | assert(StatusState.values.contains(status.state)) 40 | } 41 | } 42 | } 43 | 44 | describe("createStatus(owner, repo, sha, input)") { 45 | 46 | it("should be pending") { 47 | val input = StatusInput(StatusState.pending) 48 | val result = Await.result(api.createStatus(user, userRepo, userSha, input), TIMEOUT) 49 | assert(result.state == StatusState.pending) 50 | assert(result.target_url == None) 51 | assert(result.description == None) 52 | assert(result.context == "default") 53 | } 54 | 55 | it("should be success") { 56 | val input = StatusInput(StatusState.success, Some("http://")) 57 | val result = Await.result(api.createStatus(user, userRepo, userSha, input), TIMEOUT) 58 | assert(result.state == StatusState.success) 59 | assert(result.target_url == Some("http://")) 60 | } 61 | 62 | it("should be error") { 63 | val input = StatusInput(StatusState.error, description = Some("Description")) 64 | val result = Await.result(api.createStatus(user, userRepo, userSha, input), TIMEOUT) 65 | assert(result.state == StatusState.error) 66 | assert(result.description == Some("Description")) 67 | } 68 | 69 | it("should be failure") { 70 | val input = StatusInput(StatusState.failure, context = Some("context")) 71 | val result = Await.result(api.createStatus(user, userRepo, userSha, input), TIMEOUT) 72 | assert(result.state == StatusState.failure) 73 | assert(result.context == "context") 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/test/scala/UserOpSpec.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github 2 | package operations 3 | 4 | import exceptions._ 5 | import models._ 6 | 7 | import org.scalatest.FunSpec 8 | import org.scalatest.BeforeAndAfterAll 9 | import scala.concurrent.Await 10 | import scala.concurrent.ExecutionContext.Implicits.global 11 | 12 | class UserOpSpec extends FunSpec 13 | with api.Constants 14 | with BeforeAndAfterAll 15 | { 16 | val origin = Await.result(api.getAuthenticatedUser, TIMEOUT) 17 | 18 | override def afterAll(): Unit = { 19 | val input = UserInput( 20 | origin.name.orElse(Some("")), 21 | origin.email.orElse(Some("")), 22 | origin.blog.orElse(Some("")), 23 | origin.company.orElse(Some("")), 24 | origin.location.orElse(Some("")), 25 | Some(origin.hireable), 26 | origin.bio.orElse(Some("")) 27 | ) 28 | val user = Await.result(api.updateAuthenticatedUser(input), TIMEOUT) 29 | } 30 | describe("getUser") { 31 | it("with valid username should succeed") { 32 | val userOp = Await.result(api.getUser("sukeshni"), TIMEOUT) 33 | assert(userOp.isDefined) 34 | val user = userOp.get 35 | assert(user.login == "sukeshni") 36 | } 37 | it("with invalid username should be None") { 38 | val userOp = Await.result(api.getUser("sukeshni-wrong"), TIMEOUT) 39 | assert(userOp.isEmpty) 40 | } 41 | } 42 | 43 | describe("updateAuthenticatedUser") { 44 | ignore("if values updated correctly should succeed") { 45 | val input = new UserInput( 46 | Some("firstname lastname"), 47 | Some("test@givery.co.jp"), 48 | Some("Blog"), 49 | Some("Anywhere"), 50 | Some("Somewhere"), 51 | Some(!origin.hireable), 52 | Some("bio") 53 | ) 54 | val res = Await.result(api.updateAuthenticatedUser(input), TIMEOUT) 55 | assert(res.name.get == input.name.get) 56 | assert(res.email.getOrElse("") == input.email.get) 57 | assert(res.blog.get == input.blog.get) 58 | assert(res.company.get == input.company.get) 59 | assert(res.location.get == input.location.get) 60 | assert(res.bio.get == input.bio.get) 61 | } 62 | } 63 | 64 | describe("getAllUsers") { 65 | it("with no since parameter it should succeed") { 66 | val res = Await.result(api.getAllUsers(), TIMEOUT) 67 | showResponse(res) 68 | assert(res{0}.id == 1) 69 | } 70 | it("with valid since parameter it should succeed") { 71 | val userOp = Await.result(api.getUser("sukeshni"), TIMEOUT)//give a valid username 72 | assert(userOp.isDefined) 73 | val userOpGet = userOp.get 74 | val res = Await.result(api.getAllUsers(userOpGet.id - 1), TIMEOUT) 75 | showResponse(res) 76 | assert((res{0}.id).toLong == userOpGet.id) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/scala/WebhookOpSpec.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github 2 | package operations 3 | 4 | import models._ 5 | 6 | import org.scalatest.FunSpec 7 | import org.scalatest.BeforeAndAfter 8 | import scala.concurrent.Await 9 | 10 | class WebhookOpSpec extends FunSpec with api.Constants with BeforeAndAfter { 11 | 12 | val targetURL = "http://github-hook.herokuapp.com/hook" 13 | var nID: Long = 0; 14 | 15 | describe("listWebhooks(owner, repo)") { 16 | it("should succeed with valid owner, repo.") { 17 | val result = Await.result(api.listWebhooks(user, userRepo), TIMEOUT) 18 | assert(result.length >= 0) 19 | } 20 | } 21 | 22 | describe("createWebhook(owner, repo, input)") { 23 | it("should succeed with valid user, repo, and inputs.") { 24 | val config = WebhookConfig(targetURL) 25 | val input = WebhookCreateInput("web", config, events=Seq("*")) 26 | val res = Await.result(api.createWebhook(user, userRepo, input), TIMEOUT) 27 | showResponse(res) 28 | nID = res.id 29 | assert(res.url == "https://api.github.com/repos/" + user + "/" + userRepo + "/hooks/" + nID) 30 | assert(res.test_url == "https://api.github.com/repos/" + user + "/" + userRepo + "/hooks/" + nID + "/test") 31 | assert(res.ping_url == "https://api.github.com/repos/" + user + "/" + userRepo + "/hooks/" + nID + "/pings") 32 | assert(res.name == "web") 33 | assert(res.events == Seq("*")) 34 | assert(res.active == true) 35 | assert(res.config.url == Some(targetURL)) 36 | assert(res.config.content_type == Some("json")) 37 | assert(res.config.secret == None) 38 | assert(res.config.insecure_ssl == Some(false)) 39 | } 40 | } 41 | 42 | describe("getWebhook(owner, repo, id)") { 43 | it("should succeed with valid user, repo and id.") { 44 | Await.result(api.getWebhook(user, userRepo, nID), TIMEOUT).map { res => 45 | assert(res.id == nID) 46 | } 47 | } 48 | } 49 | 50 | describe("updateWebhook(owner, repo, id, input)") { 51 | it("should succeed updating by rewriting events.") { 52 | val input = WebhookUpdateInput(events=Some(Seq("create", "pull_request"))) 53 | val res = Await.result(api.updateWebhook(user, userRepo, nID, input), TIMEOUT) 54 | assert(res.events == Seq("create", "pull_request")) 55 | } 56 | 57 | it("should succeed updating by using add_events.") { 58 | val input = WebhookUpdateInput(add_events=Some(Seq("push"))) 59 | val res = Await.result(api.updateWebhook(user, userRepo, nID, input), TIMEOUT) 60 | assert(res.config.url == Some(targetURL)) 61 | assert(res.events == Seq("create", "pull_request", "push")) 62 | } 63 | 64 | it("should succeed updating by using remove_events.") { 65 | val input = WebhookUpdateInput(remove_events=Some(Seq("pull_request"))) 66 | val res = Await.result(api.updateWebhook(user, userRepo, nID, input), TIMEOUT) 67 | assert(res.config.url == Some(targetURL)) 68 | assert(res.events == Seq("create", "push")) 69 | } 70 | 71 | it("should succeed updating by rewriting config.") { 72 | val config = WebhookConfig(targetURL) 73 | val input = WebhookUpdateInput(Some(config)) 74 | val res = Await.result(api.updateWebhook(user, userRepo, nID, input), TIMEOUT) 75 | assert(res.config.url == Some(targetURL)) 76 | } 77 | } 78 | 79 | describe("testWebhook(owner, repo, id)") { 80 | it("should succeed with valid inputs.") { 81 | val result = Await.result(api.testWebhook(user, userRepo, nID), TIMEOUT) 82 | assert(result == true) 83 | } 84 | } 85 | 86 | describe("pingWebhook(owner, repo, id)") { 87 | it("should succeed with valid inputs.") { 88 | val result = Await.result(api.pingWebhook(user, userRepo, nID), TIMEOUT) 89 | assert(result == true) 90 | } 91 | } 92 | 93 | describe("removeWebhook(owner, repo, id)") { 94 | it("should succeed with valid inputs.") { 95 | val result = Await.result(api.removeWebhook(user, userRepo, nID), TIMEOUT) 96 | assert(result == true) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/test/scala/events/GitHubEventSpec.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github 2 | package events 3 | 4 | import org.scalatest.FunSpec 5 | import org.scalatest.Inside 6 | import org.scalatest.Matchers 7 | 8 | class GitHubEventSpec extends FunSpec with Matchers with Inside 9 | with IssueEventJson 10 | with PullRequestEventJson 11 | with PullRequestReviewEventJson 12 | with PushEventJson { 13 | 14 | describe("GitHubEvent(issue, JValue)") { 15 | val event = GitHubEvent("issues", issueEventJson) 16 | 17 | it("should yield IssueEvent") { 18 | event shouldBe a [IssueEvent] 19 | } 20 | describe("IssueEvent") { 21 | inside(event) { 22 | case e @ IssueEvent(name, _) => 23 | it("should have a name") { 24 | assert(name === "issues") 25 | } 26 | it("should have an action") { 27 | assert(e.action === models.IssueAction.opened) 28 | } 29 | it("should have an issue") { 30 | e.issue shouldBe a [models.Issue] 31 | } 32 | describe("Issue") { 33 | val issue = e.issue 34 | it("should have a number") { 35 | assert(issue.number === 2L) 36 | } 37 | it("should have a title") { 38 | assert(issue.title === "Spelling error in the README file") 39 | } 40 | it("should have a state") { 41 | assert(issue.state === models.IssueState.open) 42 | } 43 | it("should have a body") { 44 | val exp = "" 45 | assert(issue.body === exp) 46 | } 47 | } 48 | } 49 | } 50 | } 51 | 52 | describe("GitHubEvent(push, JValue)") { 53 | val event = GitHubEvent("push", pushEventJson) 54 | 55 | it("should yield PushEvent") { 56 | event shouldBe a [PushEvent] 57 | } 58 | describe("Push") { 59 | inside(event) { 60 | case e @ PushEvent(name, _) => 61 | it("should have a name") { 62 | assert(name === "push") 63 | } 64 | it("should have a ref") { 65 | assert(e.ref === "refs/heads/changes") 66 | } 67 | it("should have a base ref") { 68 | assert(e.base_ref === None) 69 | } 70 | it("should have a before") { 71 | assert(e.before === "9049f1265b7d61be4a8904a9a27120d2064dab3b") 72 | } 73 | it("should have an after") { 74 | assert(e.after === "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c") 75 | } 76 | it("should have a head commit") { 77 | e.head_commit shouldBe a [PushCommit] 78 | } 79 | describe("PushCommit") { 80 | val commit = e.head_commit 81 | it("should have a id") { 82 | assert(commit.id === "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c") 83 | } 84 | it("should have a message") { 85 | assert(commit.message === "Update README.md") 86 | } 87 | it("should have a timestamp") { 88 | assert(commit.timestamp === "2015-05-05T19:40:15-04:00") 89 | } 90 | it("should have a tree_id") { 91 | assert(commit.tree_id === "f9d2a07e9488b91af2641b26b9407fe22a451433") 92 | } 93 | it("should have a comitter") { 94 | commit.committer shouldBe a [models.User] 95 | } 96 | } 97 | it("should have a repository") { 98 | e.repository shouldBe a [models.Repository] 99 | } 100 | describe("Repository") { 101 | val repo = e.repository 102 | it("should have an id") { 103 | assert(repo.id === 35129377) 104 | } 105 | it("should have a name") { 106 | assert(repo.name === "public-repo") 107 | } 108 | it("should have a full_name") { 109 | assert(repo.full_name === "baxterthehacker/public-repo") 110 | } 111 | it("should have a owner") { 112 | repo.owner shouldBe a [models.User] 113 | } 114 | } 115 | it("should have a pusher") { 116 | e.pusher shouldBe a [models.User] 117 | } 118 | it("should have a sender") { 119 | e.sender shouldBe a [models.User] 120 | } 121 | } 122 | } 123 | } 124 | 125 | describe("GitHubEvent(pull_request, JValue)") { 126 | val event = GitHubEvent("pull_request", pullRequestEventJson) 127 | 128 | it("should yield PullRequestEvent") { 129 | event shouldBe a [PullRequestEvent] 130 | } 131 | describe("PullRequest") { 132 | inside(event) { 133 | case e @ PullRequestEvent(name, _) => 134 | it("should have a name") { 135 | assert(name === "pull_request") 136 | } 137 | it("should have a number") { 138 | assert(e.number === 1L) 139 | } 140 | it("should have an action") { 141 | assert(e.action === models.PullRequestAction.opened) 142 | } 143 | it("should have a pull request") { 144 | e.pull_request shouldBe a [models.PullRequest] 145 | } 146 | describe("PullRequest") { 147 | val pr = e.pull_request 148 | it("should have a number") { 149 | assert(pr.number === 1L) 150 | } 151 | it("should have a title") { 152 | assert(pr.title === "Update the README with new information") 153 | } 154 | it("should have a state") { 155 | assert(pr.state === models.IssueState.open) 156 | } 157 | it("should have a body") { 158 | val exp = "This is a pretty simple change that we need to pull into master." 159 | assert(pr.body === exp) 160 | } 161 | it("should have a head") { 162 | pr.head shouldBe a [models.PullRequestRef] 163 | } 164 | describe("PullRequestRef") { 165 | val head = pr.head 166 | it("should have a label") { 167 | assert(head.label === "baxterthehacker:changes") 168 | } 169 | it("should have a ref") { 170 | assert(head.ref === "changes") 171 | } 172 | it("should have a sha") { 173 | assert(head.sha === "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c") 174 | } 175 | it("should have a user") { 176 | head.user shouldBe a [models.User] 177 | } 178 | it("should have a repo") { 179 | head.repo.get shouldBe a [models.Repository] 180 | } 181 | } 182 | it("should have a base") { 183 | pr.base shouldBe a [models.PullRequestRef] 184 | } 185 | } 186 | } 187 | } 188 | } 189 | 190 | describe("GitHubEvent(pull_request_review, JValue)") { 191 | val event = GitHubEvent("pull_request_review", pullRequestReviewEventJson) 192 | 193 | it("should yield PullRequestReviewEvent") { 194 | event shouldBe a [PullRequestReviewEvent] 195 | } 196 | describe("PullRequestReviewEvent") { 197 | inside(event) { 198 | case e @ PullRequestReviewEvent(name, _) => 199 | it("should have a name") { 200 | assert(name === "pull_request_review") 201 | } 202 | it("should have an action") { 203 | assert(e.action === models.PullRequestReviewAction.submitted) 204 | } 205 | it("should have a review") { 206 | e.review shouldBe a [models.PullRequestReview] 207 | } 208 | describe("PullRequestReview") { 209 | val review = e.review 210 | it("should have an id") { 211 | assert(review.id === 2626884L) 212 | } 213 | it("should have a state") { 214 | assert(review.state === models.PullRequestReviewState.approved) 215 | } 216 | it("should have a body") { 217 | val exp = "Looks great!" 218 | assert(review.body === exp) 219 | } 220 | } 221 | it("should have a pull request") { 222 | e.pull_request shouldBe a [models.PullRequest] 223 | } 224 | describe("PullRequest") { 225 | val pr = e.pull_request 226 | it("should have a number") { 227 | assert(pr.number === 8L) 228 | } 229 | it("should have a title") { 230 | assert(pr.title === "Add a README description") 231 | } 232 | it("should have a state") { 233 | assert(pr.state === models.IssueState.open) 234 | } 235 | it("should have a body") { 236 | val exp = "Just a few more details" 237 | assert(pr.body === exp) 238 | } 239 | it("should have a head") { 240 | pr.head shouldBe a [models.PullRequestRef] 241 | } 242 | describe("PullRequestRef") { 243 | val head = pr.head 244 | it("should have a label") { 245 | assert(head.label === "skalnik:patch-2") 246 | } 247 | it("should have a ref") { 248 | assert(head.ref === "patch-2") 249 | } 250 | it("should have a sha") { 251 | assert(head.sha === "b7a1f9c27caa4e03c14a88feb56e2d4f7500aa63") 252 | } 253 | it("should have a user") { 254 | head.user shouldBe a [models.User] 255 | } 256 | it("should have a repo") { 257 | head.repo.get shouldBe a [models.Repository] 258 | } 259 | } 260 | it("should have a base") { 261 | pr.base shouldBe a [models.PullRequestRef] 262 | } 263 | } 264 | it("should have a repository") { 265 | e.repository shouldBe a [models.Repository] 266 | } 267 | describe("Repository") { 268 | val repo = e.repository 269 | it("should have an id") { 270 | assert(repo.id === 35129377L) 271 | } 272 | it("should have a name") { 273 | assert(repo.name === "public-repo") 274 | } 275 | it("should have a full_name") { 276 | assert(repo.full_name === "baxterthehacker/public-repo") 277 | } 278 | it("should have a url") { 279 | assert(repo.url === "https://api.github.com/repos/baxterthehacker/public-repo") 280 | } 281 | } 282 | it("should have a sender") { 283 | e.sender shouldBe a [models.User] 284 | } 285 | describe("User") { 286 | val user = e.sender 287 | it("should have an id") { 288 | assert(user.id === 6752317L) 289 | } 290 | it("should have a login") { 291 | assert(user.login === "baxterthehacker") 292 | } 293 | it("should have a name") { 294 | assert(user.name === None) 295 | } 296 | } 297 | } 298 | } 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /src/test/scala/events/IssueEventJson.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github 2 | package events 3 | 4 | import org.json4s.jackson.JsonMethods 5 | 6 | trait IssueEventJson { 7 | 8 | val issueEventJson = JsonMethods.parse( 9 | """ 10 | |{ 11 | | "action": "opened", 12 | | "issue": { 13 | | "url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/2", 14 | | "labels_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/2/labels{/name}", 15 | | "comments_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/2/comments", 16 | | "events_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/2/events", 17 | | "html_url": "https://github.com/baxterthehacker/public-repo/issues/2", 18 | | "id": 73464126, 19 | | "number": 2, 20 | | "title": "Spelling error in the README file", 21 | | "user": { 22 | | "login": "baxterthehacker", 23 | | "id": 6752317, 24 | | "avatar_url": "https://avatars.githubusercontent.com/u/6752317?v=3", 25 | | "gravatar_id": "", 26 | | "url": "https://api.github.com/users/baxterthehacker", 27 | | "html_url": "https://github.com/baxterthehacker", 28 | | "followers_url": "https://api.github.com/users/baxterthehacker/followers", 29 | | "following_url": "https://api.github.com/users/baxterthehacker/following{/other_user}", 30 | | "gists_url": "https://api.github.com/users/baxterthehacker/gists{/gist_id}", 31 | | "starred_url": "https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}", 32 | | "subscriptions_url": "https://api.github.com/users/baxterthehacker/subscriptions", 33 | | "organizations_url": "https://api.github.com/users/baxterthehacker/orgs", 34 | | "repos_url": "https://api.github.com/users/baxterthehacker/repos", 35 | | "events_url": "https://api.github.com/users/baxterthehacker/events{/privacy}", 36 | | "received_events_url": "https://api.github.com/users/baxterthehacker/received_events", 37 | | "type": "User", 38 | | "site_admin": false 39 | | }, 40 | | "labels": [ 41 | | { 42 | | "url": "https://api.github.com/repos/baxterthehacker/public-repo/labels/bug", 43 | | "name": "bug", 44 | | "color": "fc2929" 45 | | } 46 | | ], 47 | | "state": "open", 48 | | "locked": false, 49 | | "assignee": null, 50 | | "milestone": null, 51 | | "comments": 0, 52 | | "created_at": "2015-05-05T23:40:28Z", 53 | | "updated_at": "2015-05-05T23:40:28Z", 54 | | "closed_at": null, 55 | | "body": "" 56 | | }, 57 | | "repository": { 58 | | "id": 35129377, 59 | | "name": "public-repo", 60 | | "full_name": "baxterthehacker/public-repo", 61 | | "owner": { 62 | | "login": "baxterthehacker", 63 | | "id": 6752317, 64 | | "avatar_url": "https://avatars.githubusercontent.com/u/6752317?v=3", 65 | | "gravatar_id": "", 66 | | "url": "https://api.github.com/users/baxterthehacker", 67 | | "html_url": "https://github.com/baxterthehacker", 68 | | "followers_url": "https://api.github.com/users/baxterthehacker/followers", 69 | | "following_url": "https://api.github.com/users/baxterthehacker/following{/other_user}", 70 | | "gists_url": "https://api.github.com/users/baxterthehacker/gists{/gist_id}", 71 | | "starred_url": "https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}", 72 | | "subscriptions_url": "https://api.github.com/users/baxterthehacker/subscriptions", 73 | | "organizations_url": "https://api.github.com/users/baxterthehacker/orgs", 74 | | "repos_url": "https://api.github.com/users/baxterthehacker/repos", 75 | | "events_url": "https://api.github.com/users/baxterthehacker/events{/privacy}", 76 | | "received_events_url": "https://api.github.com/users/baxterthehacker/received_events", 77 | | "type": "User", 78 | | "site_admin": false 79 | | }, 80 | | "private": false, 81 | | "html_url": "https://github.com/baxterthehacker/public-repo", 82 | | "description": "", 83 | | "fork": false, 84 | | "url": "https://api.github.com/repos/baxterthehacker/public-repo", 85 | | "forks_url": "https://api.github.com/repos/baxterthehacker/public-repo/forks", 86 | | "keys_url": "https://api.github.com/repos/baxterthehacker/public-repo/keys{/key_id}", 87 | | "collaborators_url": "https://api.github.com/repos/baxterthehacker/public-repo/collaborators{/collaborator}", 88 | | "teams_url": "https://api.github.com/repos/baxterthehacker/public-repo/teams", 89 | | "hooks_url": "https://api.github.com/repos/baxterthehacker/public-repo/hooks", 90 | | "issue_events_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/events{/number}", 91 | | "events_url": "https://api.github.com/repos/baxterthehacker/public-repo/events", 92 | | "assignees_url": "https://api.github.com/repos/baxterthehacker/public-repo/assignees{/user}", 93 | | "branches_url": "https://api.github.com/repos/baxterthehacker/public-repo/branches{/branch}", 94 | | "tags_url": "https://api.github.com/repos/baxterthehacker/public-repo/tags", 95 | | "blobs_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/blobs{/sha}", 96 | | "git_tags_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/tags{/sha}", 97 | | "git_refs_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/refs{/sha}", 98 | | "trees_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/trees{/sha}", 99 | | "statuses_url": "https://api.github.com/repos/baxterthehacker/public-repo/statuses/{sha}", 100 | | "languages_url": "https://api.github.com/repos/baxterthehacker/public-repo/languages", 101 | | "stargazers_url": "https://api.github.com/repos/baxterthehacker/public-repo/stargazers", 102 | | "contributors_url": "https://api.github.com/repos/baxterthehacker/public-repo/contributors", 103 | | "subscribers_url": "https://api.github.com/repos/baxterthehacker/public-repo/subscribers", 104 | | "subscription_url": "https://api.github.com/repos/baxterthehacker/public-repo/subscription", 105 | | "commits_url": "https://api.github.com/repos/baxterthehacker/public-repo/commits{/sha}", 106 | | "git_commits_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/commits{/sha}", 107 | | "comments_url": "https://api.github.com/repos/baxterthehacker/public-repo/comments{/number}", 108 | | "issue_comment_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/comments{/number}", 109 | | "contents_url": "https://api.github.com/repos/baxterthehacker/public-repo/contents/{+path}", 110 | | "compare_url": "https://api.github.com/repos/baxterthehacker/public-repo/compare/{base}...{head}", 111 | | "merges_url": "https://api.github.com/repos/baxterthehacker/public-repo/merges", 112 | | "archive_url": "https://api.github.com/repos/baxterthehacker/public-repo/{archive_format}{/ref}", 113 | | "downloads_url": "https://api.github.com/repos/baxterthehacker/public-repo/downloads", 114 | | "issues_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues{/number}", 115 | | "pulls_url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls{/number}", 116 | | "milestones_url": "https://api.github.com/repos/baxterthehacker/public-repo/milestones{/number}", 117 | | "notifications_url": "https://api.github.com/repos/baxterthehacker/public-repo/notifications{?since,all,participating}", 118 | | "labels_url": "https://api.github.com/repos/baxterthehacker/public-repo/labels{/name}", 119 | | "releases_url": "https://api.github.com/repos/baxterthehacker/public-repo/releases{/id}", 120 | | "created_at": "2015-05-05T23:40:12Z", 121 | | "updated_at": "2015-05-05T23:40:12Z", 122 | | "pushed_at": "2015-05-05T23:40:27Z", 123 | | "git_url": "git://github.com/baxterthehacker/public-repo.git", 124 | | "ssh_url": "git@github.com:baxterthehacker/public-repo.git", 125 | | "clone_url": "https://github.com/baxterthehacker/public-repo.git", 126 | | "svn_url": "https://github.com/baxterthehacker/public-repo", 127 | | "homepage": null, 128 | | "size": 0, 129 | | "stargazers_count": 0, 130 | | "watchers_count": 0, 131 | | "language": null, 132 | | "has_issues": true, 133 | | "has_downloads": true, 134 | | "has_wiki": true, 135 | | "has_pages": true, 136 | | "forks_count": 0, 137 | | "mirror_url": null, 138 | | "open_issues_count": 2, 139 | | "forks": 0, 140 | | "open_issues": 2, 141 | | "watchers": 0, 142 | | "default_branch": "master" 143 | | }, 144 | | "sender": { 145 | | "login": "baxterthehacker", 146 | | "id": 6752317, 147 | | "avatar_url": "https://avatars.githubusercontent.com/u/6752317?v=3", 148 | | "gravatar_id": "", 149 | | "url": "https://api.github.com/users/baxterthehacker", 150 | | "html_url": "https://github.com/baxterthehacker", 151 | | "followers_url": "https://api.github.com/users/baxterthehacker/followers", 152 | | "following_url": "https://api.github.com/users/baxterthehacker/following{/other_user}", 153 | | "gists_url": "https://api.github.com/users/baxterthehacker/gists{/gist_id}", 154 | | "starred_url": "https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}", 155 | | "subscriptions_url": "https://api.github.com/users/baxterthehacker/subscriptions", 156 | | "organizations_url": "https://api.github.com/users/baxterthehacker/orgs", 157 | | "repos_url": "https://api.github.com/users/baxterthehacker/repos", 158 | | "events_url": "https://api.github.com/users/baxterthehacker/events{/privacy}", 159 | | "received_events_url": "https://api.github.com/users/baxterthehacker/received_events", 160 | | "type": "User", 161 | | "site_admin": false 162 | | } 163 | |}""".stripMargin) 164 | } 165 | -------------------------------------------------------------------------------- /src/test/scala/events/PushEventJson.scala: -------------------------------------------------------------------------------- 1 | package codecheck.github 2 | package events 3 | 4 | import org.json4s.jackson.JsonMethods 5 | 6 | trait PushEventJson { 7 | 8 | val pushEventJson = JsonMethods.parse( 9 | """{ 10 | | "ref": "refs/heads/changes", 11 | | "before": "9049f1265b7d61be4a8904a9a27120d2064dab3b", 12 | | "after": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c", 13 | | "created": false, 14 | | "deleted": false, 15 | | "forced": false, 16 | | "base_ref": null, 17 | | "compare": "https://github.com/baxterthehacker/public-repo/compare/9049f1265b7d...0d1a26e67d8f", 18 | | "commits": [ 19 | | { 20 | | "id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c", 21 | | "tree_id": "f9d2a07e9488b91af2641b26b9407fe22a451433", 22 | | "distinct": true, 23 | | "message": "Update README.md", 24 | | "timestamp": "2015-05-05T19:40:15-04:00", 25 | | "url": "https://github.com/baxterthehacker/public-repo/commit/0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c", 26 | | "author": { 27 | | "name": "baxterthehacker", 28 | | "email": "baxterthehacker@users.noreply.github.com", 29 | | "username": "baxterthehacker" 30 | | }, 31 | | "committer": { 32 | | "name": "baxterthehacker", 33 | | "email": "baxterthehacker@users.noreply.github.com", 34 | | "username": "baxterthehacker" 35 | | }, 36 | | "added": [ 37 | | 38 | | ], 39 | | "removed": [ 40 | | 41 | | ], 42 | | "modified": [ 43 | | "README.md" 44 | | ] 45 | | } 46 | | ], 47 | | "head_commit": { 48 | | "id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c", 49 | | "tree_id": "f9d2a07e9488b91af2641b26b9407fe22a451433", 50 | | "distinct": true, 51 | | "message": "Update README.md", 52 | | "timestamp": "2015-05-05T19:40:15-04:00", 53 | | "url": "https://github.com/baxterthehacker/public-repo/commit/0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c", 54 | | "author": { 55 | | "name": "baxterthehacker", 56 | | "email": "baxterthehacker@users.noreply.github.com", 57 | | "username": "baxterthehacker" 58 | | }, 59 | | "committer": { 60 | | "name": "baxterthehacker", 61 | | "email": "baxterthehacker@users.noreply.github.com", 62 | | "username": "baxterthehacker" 63 | | }, 64 | | "added": [ 65 | | 66 | | ], 67 | | "removed": [ 68 | | 69 | | ], 70 | | "modified": [ 71 | | "README.md" 72 | | ] 73 | | }, 74 | | "repository": { 75 | | "id": 35129377, 76 | | "name": "public-repo", 77 | | "full_name": "baxterthehacker/public-repo", 78 | | "owner": { 79 | | "name": "baxterthehacker", 80 | | "email": "baxterthehacker@users.noreply.github.com" 81 | | }, 82 | | "private": false, 83 | | "html_url": "https://github.com/baxterthehacker/public-repo", 84 | | "description": "", 85 | | "fork": false, 86 | | "url": "https://github.com/baxterthehacker/public-repo", 87 | | "forks_url": "https://api.github.com/repos/baxterthehacker/public-repo/forks", 88 | | "keys_url": "https://api.github.com/repos/baxterthehacker/public-repo/keys{/key_id}", 89 | | "collaborators_url": "https://api.github.com/repos/baxterthehacker/public-repo/collaborators{/collaborator}", 90 | | "teams_url": "https://api.github.com/repos/baxterthehacker/public-repo/teams", 91 | | "hooks_url": "https://api.github.com/repos/baxterthehacker/public-repo/hooks", 92 | | "issue_events_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/events{/number}", 93 | | "events_url": "https://api.github.com/repos/baxterthehacker/public-repo/events", 94 | | "assignees_url": "https://api.github.com/repos/baxterthehacker/public-repo/assignees{/user}", 95 | | "branches_url": "https://api.github.com/repos/baxterthehacker/public-repo/branches{/branch}", 96 | | "tags_url": "https://api.github.com/repos/baxterthehacker/public-repo/tags", 97 | | "blobs_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/blobs{/sha}", 98 | | "git_tags_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/tags{/sha}", 99 | | "git_refs_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/refs{/sha}", 100 | | "trees_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/trees{/sha}", 101 | | "statuses_url": "https://api.github.com/repos/baxterthehacker/public-repo/statuses/{sha}", 102 | | "languages_url": "https://api.github.com/repos/baxterthehacker/public-repo/languages", 103 | | "stargazers_url": "https://api.github.com/repos/baxterthehacker/public-repo/stargazers", 104 | | "contributors_url": "https://api.github.com/repos/baxterthehacker/public-repo/contributors", 105 | | "subscribers_url": "https://api.github.com/repos/baxterthehacker/public-repo/subscribers", 106 | | "subscription_url": "https://api.github.com/repos/baxterthehacker/public-repo/subscription", 107 | | "commits_url": "https://api.github.com/repos/baxterthehacker/public-repo/commits{/sha}", 108 | | "git_commits_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/commits{/sha}", 109 | | "comments_url": "https://api.github.com/repos/baxterthehacker/public-repo/comments{/number}", 110 | | "issue_comment_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/comments{/number}", 111 | | "contents_url": "https://api.github.com/repos/baxterthehacker/public-repo/contents/{+path}", 112 | | "compare_url": "https://api.github.com/repos/baxterthehacker/public-repo/compare/{base}...{head}", 113 | | "merges_url": "https://api.github.com/repos/baxterthehacker/public-repo/merges", 114 | | "archive_url": "https://api.github.com/repos/baxterthehacker/public-repo/{archive_format}{/ref}", 115 | | "downloads_url": "https://api.github.com/repos/baxterthehacker/public-repo/downloads", 116 | | "issues_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues{/number}", 117 | | "pulls_url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls{/number}", 118 | | "milestones_url": "https://api.github.com/repos/baxterthehacker/public-repo/milestones{/number}", 119 | | "notifications_url": "https://api.github.com/repos/baxterthehacker/public-repo/notifications{?since,all,participating}", 120 | | "labels_url": "https://api.github.com/repos/baxterthehacker/public-repo/labels{/name}", 121 | | "releases_url": "https://api.github.com/repos/baxterthehacker/public-repo/releases{/id}", 122 | | "created_at": 1430869212, 123 | | "updated_at": "2015-05-05T23:40:12Z", 124 | | "pushed_at": 1430869217, 125 | | "git_url": "git://github.com/baxterthehacker/public-repo.git", 126 | | "ssh_url": "git@github.com:baxterthehacker/public-repo.git", 127 | | "clone_url": "https://github.com/baxterthehacker/public-repo.git", 128 | | "svn_url": "https://github.com/baxterthehacker/public-repo", 129 | | "homepage": null, 130 | | "size": 0, 131 | | "stargazers_count": 0, 132 | | "watchers_count": 0, 133 | | "language": null, 134 | | "has_issues": true, 135 | | "has_downloads": true, 136 | | "has_wiki": true, 137 | | "has_pages": true, 138 | | "forks_count": 0, 139 | | "mirror_url": null, 140 | | "open_issues_count": 0, 141 | | "forks": 0, 142 | | "open_issues": 0, 143 | | "watchers": 0, 144 | | "default_branch": "master", 145 | | "stargazers": 0, 146 | | "master_branch": "master" 147 | | }, 148 | | "pusher": { 149 | | "name": "baxterthehacker", 150 | | "email": "baxterthehacker@users.noreply.github.com" 151 | | }, 152 | | "sender": { 153 | | "login": "baxterthehacker", 154 | | "id": 6752317, 155 | | "avatar_url": "https://avatars.githubusercontent.com/u/6752317?v=3", 156 | | "gravatar_id": "", 157 | | "url": "https://api.github.com/users/baxterthehacker", 158 | | "html_url": "https://github.com/baxterthehacker", 159 | | "followers_url": "https://api.github.com/users/baxterthehacker/followers", 160 | | "following_url": "https://api.github.com/users/baxterthehacker/following{/other_user}", 161 | | "gists_url": "https://api.github.com/users/baxterthehacker/gists{/gist_id}", 162 | | "starred_url": "https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}", 163 | | "subscriptions_url": "https://api.github.com/users/baxterthehacker/subscriptions", 164 | | "organizations_url": "https://api.github.com/users/baxterthehacker/orgs", 165 | | "repos_url": "https://api.github.com/users/baxterthehacker/repos", 166 | | "events_url": "https://api.github.com/users/baxterthehacker/events{/privacy}", 167 | | "received_events_url": "https://api.github.com/users/baxterthehacker/received_events", 168 | | "type": "User", 169 | | "site_admin": false 170 | | } 171 | |} 172 | |""".stripMargin) 173 | } 174 | -------------------------------------------------------------------------------- /test.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | This readme documents the changes to the tests. 3 | 4 | ## Initial Setup 5 | 1. Create new repo for test on your GitHub account 6 | 1. Export username and repository that exists under your name. 7 | 2. If you have no yet exported your Github token, create one [here](https://github.com/settings/tokens) and export it. 8 | 9 | ``` bash 10 | export GITHUB_TOKEN=[Your GitHub Token] 11 | export GITHUB_USER=[Your User Name] 12 | export GITHUB_REPO=[Your Repo name that exists] 13 | ``` 14 | 15 | The repo specified in GITHUB_REPO will be updated by test. 16 | I strongly recommend to create new repo for this. 17 | 18 | ## Optional settings 19 | 20 | ``` 21 | export DEBUG=true 22 | ``` 23 | 24 | If DEBUG=true inv env, you can see the response JSON data in console. 25 | 26 | The following variables are for futureproofing. Generally won't need to be modified. 27 | - otherUser 28 | -- Another user's (not yourself) username. 29 | - otherUserInvalid 30 | -- An invalid username. 31 | - organizationInvalid 32 | -- An invalid organization. 33 | - repoInvalid 34 | -- An invalid repo. 35 | 36 | The following variables should not be changed. 37 | - organization 38 | -- This is by default set to our dummy test organization "celestialbeings". 39 | - repo 40 | -- This is by default set to the dummy est repo "test-repo". 41 | 42 | ## Random Generator 43 | The random string generator is located in Constants.scala and uses words from the wordBank array. It has three methods. 44 | - generateRandomString() 45 | -- Returns a String with three random words seperated by spaces. 46 | - generateRandomWord() 47 | -- Returns a String with a single random word. 48 | - generatedRandomInt() 49 | -- Returns a random Int from 0 to 999. This was added to avoid having to import the Random class in every file (so it is bundled with Constants) 50 | Use these to generate random field values to test create and update functions. 51 | --------------------------------------------------------------------------------