├── CHANGELOG.md ├── .gitignore ├── CONTRIBUTING.md ├── .travis.yml ├── package.json ├── elm.json ├── README.md ├── LICENSE └── src └── Github.elm /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # main 2 | 3 | - Added getBlobAsBase64 and createBlob 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /elm-stuff/ 3 | /package-lock.json 4 | /documentation.json 5 | /.idea -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Development environment 3 | 4 | ```sh 5 | npm install 6 | npm watch 7 | ``` 8 | 9 | Running tests: 10 | ```sh 11 | npm test 12 | ``` 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | node_js: "14" 5 | 6 | cache: 7 | directories: 8 | - node_modules 9 | 10 | install: 11 | - npm install 12 | 13 | script: 14 | - npm test 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elm-github-v3", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "build": "elm make", 7 | "build-docs": "elm make --docs=documentation.json", 8 | "test": "npm run-script build && npm run-script build-docs && npm run-script check && elm diff", 9 | "check": "elm-format --validate .", 10 | "watch": "chokidar --initial elm.json 'src/**/*.elm' 'tests/**/*.elm' 'example/**/*.elm' -c 'npm test'" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "chokidar-cli": "^3.0.0", 17 | "elm": "^0.19.1-5", 18 | "elm-format": "0.8.5" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "avh4/elm-github-v3", 4 | "summary": "Unofficial GitHub v3 API for Elm", 5 | "license": "MIT", 6 | "version": "2.0.0", 7 | "exposed-modules": [ 8 | "Github" 9 | ], 10 | "elm-version": "0.19.0 <= v < 0.20.0", 11 | "dependencies": { 12 | "MartinSStewart/elm-nonempty-string": "2.0.0 <= v < 3.0.0", 13 | "elm/bytes": "1.0.8 <= v < 2.0.0", 14 | "elm/core": "1.0.0 <= v < 2.0.0", 15 | "elm/http": "2.0.0 <= v < 3.0.0", 16 | "elm/json": "1.1.3 <= v < 2.0.0", 17 | "elm/time": "1.0.0 <= v < 2.0.0", 18 | "elm/url": "1.0.0 <= v < 2.0.0", 19 | "mgold/elm-nonempty-list": "4.2.0 <= v < 5.0.0", 20 | "rtfeldman/elm-iso8601-date-strings": "1.1.2 <= v < 2.0.0", 21 | "truqu/elm-base64": "2.0.4 <= v < 3.0.0" 22 | }, 23 | "test-dependencies": {} 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/avh4/elm-github-v3.svg?branch=master)](https://travis-ci.org/avh4/elm-github-v3) 2 | [![Latest Version](https://img.shields.io/elm-package/v/avh4/elm-github-v3.svg?label=version)](https://package.elm-lang.org/packages/avh4/elm-github-v3/latest/) 3 | 4 | 5 | # elm-github-v3 6 | 7 | This is an unofficial Elm wrapper for the [GitHub REST v3 API](https://developer.github.com/v3/). 8 | The implementation is currently very incomplete 9 | (I've only implemented the exact requests, input parameters, and output decode that I've needed), 10 | but I decided to publish this in case it can save others some work. 11 | Pull requests to make the implementation more complete are welcome. 12 | 13 | 14 | ## Example usage 15 | 16 | ```sh 17 | elm install avh4/elm-github-v3 18 | ``` 19 | 20 | ```elm 21 | import Github 22 | 23 | getPullRequestTitles : Cmd (Result String (List String)) 24 | getPullRequestTitles = 25 | Github.getPullRequests 26 | { authToken = "123..." 27 | , repo = "avh4/elm-format" 28 | } 29 | |> Task.map (List.map .title) 30 | |> Task.attempt identity 31 | ``` 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2020 Aaron VonderHaar 4 | Copyright (c) 2020 James Carlson 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/Github.elm: -------------------------------------------------------------------------------- 1 | module Github exposing 2 | ( OAuthToken, oauthToken, oauthTokenToString, AccessTokenResponse, oauthLink, OAuthCode, oauthCode, oauthCodeToString, ClientId, clientId, clientIdToString, ClientSecret, clientSecret, clientSecretToString, getAccessToken, Scope(..), scopeFromString, scopeToString 3 | , getRepository, getContents, Owner, owner, ownerToString, updateFileContents 4 | , Branch, branch, branchToString, getBranches, getBranch, updateBranch, listTags, createBranch, getBranchZip, getTag, getCommit, createCommit, getCommitZip, sha, shaToString, ShaHash, CommitSha, TreeSha, Content(..), ContentType(..), DirectoryEntry, createTree 5 | , PullRequest, getPullRequests, getPullRequest, createPullRequest, createFork 6 | , getComments, createComment, createIssue 7 | ) 8 | 9 | {-| 10 | 11 | 12 | ## Authorization 13 | 14 | @docs OAuthToken, oauthToken, oauthTokenToString, AccessTokenResponse, oauthLink, OAuthCode, oauthCode, oauthCodeToString, ClientId, clientId, clientIdToString, ClientSecret, clientSecret, clientSecretToString, getAccessToken, Scope, scopeFromString, scopeToString 15 | 16 | 17 | ## Get repository 18 | 19 | @docs getRepository, getContents, Owner, owner, ownerToString, updateFileContents 20 | 21 | 22 | ## Work with git 23 | 24 | @docs Branch, branch, branchToString, getBranches, getBranch, updateBranch, listTags, createBranch, getBranchZip, getTag, getCommit, createCommit, getCommitZip, sha, shaToString, ShaHash, CommitSha, TreeSha, Content, ContentType, DirectoryEntry, createTree 25 | 26 | 27 | ## Pull request 28 | 29 | @docs PullRequest, getPullRequests, getPullRequest, createPullRequest, createFork 30 | 31 | 32 | ## Issues 33 | 34 | @docs getComments, createComment, createIssue 35 | 36 | -} 37 | 38 | import Base64 39 | import Bytes exposing (Bytes) 40 | import Dict 41 | import Http 42 | import Iso8601 43 | import Json.Decode 44 | import Json.Encode 45 | import List.Nonempty exposing (Nonempty) 46 | import String.Nonempty exposing (NonemptyString) 47 | import Task exposing (Task) 48 | import Time 49 | import Url exposing (Url) 50 | import Url.Builder 51 | 52 | 53 | {-| See 54 | 55 | NOTE: Not all input options and output fields are supported yet. Pull requests adding more complete support are welcome. 56 | 57 | -} 58 | getRepository : 59 | { authToken : OAuthToken 60 | , owner : Owner 61 | , repo : String 62 | } 63 | -> Task Http.Error { defaultBranch : Branch } 64 | getRepository params = 65 | let 66 | decoder = 67 | Json.Decode.map (\defaultBranch -> { defaultBranch = defaultBranch }) 68 | (Json.Decode.field "default_branch" decodeBranch) 69 | in 70 | Http.task 71 | { method = "GET" 72 | , headers = [ authorizationHeader params.authToken ] 73 | , url = "https://api.github.com/repos/" ++ ownerToString params.owner ++ "/" ++ params.repo 74 | , body = Http.emptyBody 75 | , resolver = jsonResolver decoder 76 | , timeout = Nothing 77 | } 78 | 79 | 80 | {-| Get all branches for a git repo. 81 | -} 82 | getBranches : 83 | { authToken : OAuthToken 84 | , owner : Owner 85 | , repo : String 86 | } 87 | -> Task Http.Error (List { name : String, sha : ShaHash CommitSha }) 88 | getBranches params = 89 | let 90 | decoder = 91 | Json.Decode.map2 (\name sha_ -> { name = name, sha = sha_ }) 92 | (Json.Decode.field "name" Json.Decode.string) 93 | (Json.Decode.at [ "commit", "sha" ] decodeSha) 94 | in 95 | Http.task 96 | { method = "GET" 97 | , headers = [ authorizationHeader params.authToken ] 98 | , url = 99 | Url.Builder.crossOrigin githubApiDomain 100 | [ "repos", ownerToString params.owner, params.repo, "branches" ] 101 | [] 102 | , body = Http.emptyBody 103 | , resolver = jsonResolver (Json.Decode.list decoder) 104 | , timeout = Nothing 105 | } 106 | 107 | 108 | {-| authToken is Maybe here because it seems like there can be problems request a zip from a public repo if you provide authentication. 109 | -} 110 | getBranchZip : 111 | { authToken : Maybe OAuthToken 112 | , owner : Owner 113 | , repo : String 114 | , branchName : Maybe Branch 115 | } 116 | -> Task Http.Error Bytes 117 | getBranchZip params = 118 | Http.task 119 | { method = "GET" 120 | , headers = 121 | case params.authToken of 122 | Just authToken -> 123 | [ authorizationHeader authToken ] 124 | 125 | Nothing -> 126 | [] 127 | , url = 128 | Url.Builder.crossOrigin githubApiDomain 129 | ("repos" 130 | :: ownerToString params.owner 131 | :: params.repo 132 | :: "zipball" 133 | :: (case params.branchName of 134 | Just branchName -> 135 | [ branchToString branchName ] 136 | 137 | Nothing -> 138 | [] 139 | ) 140 | ) 141 | [] 142 | , body = Http.emptyBody 143 | , resolver = bytesResolver 144 | , timeout = Nothing 145 | } 146 | 147 | 148 | {-| -} 149 | getCommitZip : { authToken : OAuthToken, owner : Owner, repo : String, sha : ShaHash CommitSha } -> Task Http.Error Bytes 150 | getCommitZip params = 151 | Http.task 152 | { method = "GET" 153 | , headers = [ authorizationHeader params.authToken ] 154 | , url = "https://github.com/" ++ ownerToString params.owner ++ "/" ++ params.repo ++ "/archive/" ++ shaToString params.sha ++ ".zip" 155 | , body = Http.emptyBody 156 | , resolver = bytesResolver 157 | , timeout = Nothing 158 | } 159 | 160 | 161 | {-| See 162 | 163 | NOTE: Not all input options and output fields are supported yet. Pull requests adding more complete support are welcome. 164 | 165 | -} 166 | getCommit : 167 | { authToken : OAuthToken 168 | , owner : Owner 169 | , repo : String 170 | , sha : ShaHash CommitSha 171 | } 172 | -> Task Http.Error (ShaHash TreeSha) 173 | getCommit params = 174 | let 175 | decoder = 176 | Json.Decode.at [ "tree", "sha" ] decodeSha 177 | in 178 | Http.task 179 | { method = "GET" 180 | , headers = [ authorizationHeader params.authToken ] 181 | , url = "https://api.github.com/repos/" ++ ownerToString params.owner ++ "/" ++ params.repo ++ "/git/commits/" ++ shaToString params.sha 182 | , body = Http.emptyBody 183 | , resolver = jsonResolver decoder 184 | , timeout = Nothing 185 | } 186 | 187 | 188 | {-| See 189 | 190 | NOTE: Not all input options and output fields are supported yet. Pull requests adding more complete support are welcome. 191 | 192 | -} 193 | createCommit : 194 | { authToken : OAuthToken 195 | , owner : Owner 196 | , repo : String 197 | , message : String 198 | , tree : ShaHash TreeSha 199 | , parents : List (ShaHash CommitSha) 200 | } 201 | -> Task Http.Error (ShaHash CommitSha) 202 | createCommit params = 203 | let 204 | decoder = 205 | Json.Decode.field "sha" Json.Decode.string 206 | |> Json.Decode.map sha 207 | in 208 | Http.task 209 | { method = "POST" 210 | , headers = [ authorizationHeader params.authToken ] 211 | , url = "https://api.github.com/repos/" ++ ownerToString params.owner ++ "/" ++ params.repo ++ "/git/commits" 212 | , body = 213 | Http.jsonBody 214 | (Json.Encode.object 215 | [ ( "message", Json.Encode.string params.message ) 216 | , ( "tree", encodeSha params.tree ) 217 | , ( "parents", Json.Encode.list encodeSha params.parents ) 218 | ] 219 | ) 220 | , resolver = jsonResolver decoder 221 | , timeout = Nothing 222 | } 223 | 224 | 225 | {-| See 226 | 227 | NOTE: Not all input options and output fields are supported yet. Pull requests adding more complete support are welcome. 228 | 229 | -} 230 | getBranch : 231 | { authToken : OAuthToken 232 | , owner : Owner 233 | , repo : String 234 | , branchName : Branch 235 | } 236 | -> Task Http.Error (ShaHash CommitSha) 237 | getBranch params = 238 | let 239 | decoder = 240 | Json.Decode.at [ "object", "sha" ] decodeSha 241 | in 242 | Http.task 243 | { method = "GET" 244 | , headers = [ authorizationHeader params.authToken ] 245 | , url = "https://api.github.com/repos/" ++ ownerToString params.owner ++ "/" ++ params.repo ++ "/git/refs/heads/" ++ branchToString params.branchName 246 | , body = Http.emptyBody 247 | , resolver = jsonResolver decoder 248 | , timeout = Nothing 249 | } 250 | 251 | 252 | {-| See 253 | 254 | NOTE: Not all input options and output fields are supported yet. Pull requests adding more complete support are welcome. 255 | 256 | -} 257 | updateBranch : 258 | { authToken : OAuthToken 259 | , owner : Owner 260 | , repo : String 261 | , branchName : Branch 262 | , sha : ShaHash CommitSha 263 | , force : Bool 264 | } 265 | -> Task Http.Error (ShaHash CommitSha) 266 | updateBranch params = 267 | Http.task 268 | { method = "PATCH" 269 | , headers = [ authorizationHeader params.authToken ] 270 | , url = "https://api.github.com/repos/" ++ ownerToString params.owner ++ "/" ++ params.repo ++ "/git/refs/heads/" ++ branchToString params.branchName 271 | , body = 272 | Http.jsonBody 273 | (Json.Encode.object 274 | [ ( "sha", Json.Encode.string (shaToString params.sha) ), ( "force", Json.Encode.bool params.force ) ] 275 | ) 276 | , resolver = jsonResolver referenceDecoder 277 | , timeout = Nothing 278 | } 279 | 280 | 281 | {-| See 282 | 283 | NOTE: Not all input options and output fields are supported yet. Pull requests adding more complete support are welcome. 284 | 285 | -} 286 | getTag : 287 | { authToken : OAuthToken 288 | , owner : Owner 289 | , repo : String 290 | , tagName : String 291 | } 292 | -> Task Http.Error (ShaHash CommitSha) 293 | getTag params = 294 | let 295 | decoder = 296 | Json.Decode.at [ "object", "sha" ] decodeSha 297 | in 298 | Http.task 299 | { method = "GET" 300 | , headers = [ authorizationHeader params.authToken ] 301 | , url = "https://api.github.com/repos/" ++ ownerToString params.owner ++ "/" ++ params.repo ++ "/git/refs/tags/" ++ params.tagName 302 | , body = Http.emptyBody 303 | , resolver = jsonResolver decoder 304 | , timeout = Nothing 305 | } 306 | 307 | 308 | {-| See 309 | 310 | NOTE: Not all input options and output fields are supported yet. Pull requests adding more complete support are welcome. 311 | 312 | -} 313 | createTree : 314 | { authToken : OAuthToken 315 | , owner : Owner 316 | , repo : String 317 | , treeNodes : Nonempty { path : String, content : String } 318 | , baseTree : Maybe (ShaHash TreeSha) 319 | } 320 | -> Task Http.Error { treeSha : ShaHash TreeSha } 321 | createTree params = 322 | let 323 | decoder = 324 | Json.Decode.field "sha" decodeSha 325 | |> Json.Decode.map (\treeSha -> { treeSha = treeSha }) 326 | in 327 | Http.task 328 | { method = "POST" 329 | , headers = [ authorizationHeader params.authToken ] 330 | , url = "https://api.github.com/repos/" ++ ownerToString params.owner ++ "/" ++ params.repo ++ "/git/trees" 331 | , body = 332 | ( "tree", Json.Encode.list encodeTreeNode (List.Nonempty.toList params.treeNodes) ) 333 | :: (case params.baseTree of 334 | Just baseTree -> 335 | [ ( "base_tree", encodeSha baseTree ) ] 336 | 337 | Nothing -> 338 | [] 339 | ) 340 | |> Json.Encode.object 341 | |> Http.jsonBody 342 | , resolver = jsonResolver decoder 343 | , timeout = Nothing 344 | } 345 | 346 | 347 | encodeTreeNode : { path : String, content : String } -> Json.Encode.Value 348 | encodeTreeNode treeNode = 349 | ( "path", Json.Encode.string treeNode.path ) 350 | :: ( "mode", Json.Encode.string "100644" ) 351 | :: ( "type", Json.Encode.string "blob" ) 352 | :: ( "content", Json.Encode.string treeNode.content ) 353 | :: [] 354 | |> Json.Encode.object 355 | 356 | 357 | referenceDecoder = 358 | Json.Decode.at [ "object", "sha" ] Json.Decode.string 359 | |> Json.Decode.map sha 360 | 361 | 362 | type alias Tag = 363 | { name : String 364 | , commitSha : ShaHash CommitSha 365 | , nodeId : String 366 | } 367 | 368 | 369 | decodeTag : Json.Decode.Decoder Tag 370 | decodeTag = 371 | Json.Decode.map3 Tag 372 | (Json.Decode.field "name" Json.Decode.string) 373 | (Json.Decode.at [ "commit", "sha" ] decodeSha) 374 | (Json.Decode.field "node_id" Json.Decode.string) 375 | 376 | 377 | {-| See 378 | 379 | NOTE: Not all input options and output fields are supported yet. Pull requests adding more complete support are welcome. 380 | 381 | -} 382 | listTags : 383 | { authToken : OAuthToken 384 | , owner : Owner 385 | , repo : String 386 | } 387 | -> Task Http.Error (List Tag) 388 | listTags params = 389 | Http.task 390 | { method = "GET" 391 | , headers = [ authorizationHeader params.authToken ] 392 | , url = "https://api.github.com/repos/" ++ ownerToString params.owner ++ "/" ++ params.repo ++ "/tags" 393 | , body = Http.emptyBody 394 | , resolver = jsonResolver (Json.Decode.list decodeTag) 395 | , timeout = Nothing 396 | } 397 | 398 | 399 | {-| See 400 | 401 | NOTE: Not all input options and output fields are supported yet. Pull requests adding more complete support are welcome. 402 | 403 | -} 404 | createBranch : 405 | { authToken : OAuthToken 406 | , owner : Owner 407 | , repo : String 408 | , branchName : Branch 409 | , sha : ShaHash CommitSha 410 | } 411 | -> Task Http.Error () 412 | createBranch params = 413 | let 414 | decoder = 415 | Json.Decode.succeed () 416 | in 417 | Http.task 418 | { method = "POST" 419 | , headers = [ authorizationHeader params.authToken ] 420 | , url = 421 | Url.Builder.crossOrigin 422 | githubApiDomain 423 | [ "repos", ownerToString params.owner, params.repo, "git", "refs" ] 424 | [] 425 | , body = 426 | Http.jsonBody 427 | (Json.Encode.object 428 | [ ( "ref", Json.Encode.string ("refs/heads/" ++ branchToString params.branchName) ) 429 | , ( "sha", encodeSha params.sha ) 430 | ] 431 | ) 432 | , resolver = jsonResolver decoder 433 | , timeout = Nothing 434 | } 435 | 436 | 437 | {-| The data returned by [`getPullRequests`](#getPullRequests). 438 | -} 439 | type alias PullRequest = 440 | { number : Int 441 | , title : String 442 | } 443 | 444 | 445 | decodePullRequest = 446 | Json.Decode.map2 447 | PullRequest 448 | (Json.Decode.at [ "number" ] Json.Decode.int) 449 | (Json.Decode.at [ "title" ] Json.Decode.string) 450 | 451 | 452 | {-| See 453 | 454 | NOTE: Not all input options and output fields are supported yet. Pull requests adding more complete support are welcome. 455 | 456 | -} 457 | getPullRequests : 458 | { authToken : OAuthToken 459 | , repo : String 460 | } 461 | -> Task Http.Error (List PullRequest) 462 | getPullRequests params = 463 | Http.task 464 | { method = "GET" 465 | , headers = [ authorizationHeader params.authToken ] 466 | , url = "https://api.github.com/repos/" ++ params.repo ++ "/pulls" 467 | , body = Http.emptyBody 468 | , resolver = jsonResolver (Json.Decode.list decodePullRequest) 469 | , timeout = Nothing 470 | } 471 | 472 | 473 | {-| See 474 | 475 | NOTE: Not all input options and output fields are supported yet. Pull requests adding more complete support are welcome. 476 | 477 | -} 478 | getPullRequest : 479 | { authToken : OAuthToken 480 | , repo : String 481 | , number : Int 482 | } 483 | -> 484 | Task 485 | Http.Error 486 | { head : { ref : String, sha : ShaHash CommitSha } } 487 | getPullRequest params = 488 | let 489 | decoder = 490 | Json.Decode.map2 491 | (\headRef headSha -> 492 | { head = 493 | { ref = headRef 494 | , sha = sha headSha 495 | } 496 | } 497 | ) 498 | (Json.Decode.at [ "head", "ref" ] Json.Decode.string) 499 | (Json.Decode.at [ "head", "sha" ] Json.Decode.string) 500 | in 501 | Http.task 502 | { method = "GET" 503 | , headers = [ authorizationHeader params.authToken ] 504 | , url = "https://api.github.com/repos/" ++ params.repo ++ "/pulls/" ++ String.fromInt params.number 505 | , body = Http.emptyBody 506 | , resolver = jsonResolver decoder 507 | , timeout = Nothing 508 | } 509 | 510 | 511 | {-| See 512 | 513 | NOTE: Not all input options and output fields are supported yet. Pull requests adding more complete support are welcome. 514 | 515 | -} 516 | createPullRequest : 517 | { authToken : OAuthToken 518 | , destinationOwner : Owner 519 | , destinationRepo : String 520 | , destinationBranch : Branch 521 | , sourceBranchOwner : Owner 522 | , sourceBranch : Branch 523 | , title : String 524 | , description : String 525 | } 526 | -> Task Http.Error { apiUrl : String, htmlUrl : String } 527 | createPullRequest params = 528 | let 529 | decoder = 530 | Json.Decode.map2 (\url htmlUrl -> { apiUrl = url, htmlUrl = htmlUrl }) 531 | (Json.Decode.field "url" Json.Decode.string) 532 | (Json.Decode.field "html_url" Json.Decode.string) 533 | in 534 | Http.task 535 | { method = "POST" 536 | , headers = [ authorizationHeader params.authToken ] 537 | , url = "https://api.github.com/repos/" ++ ownerToString params.destinationOwner ++ "/" ++ params.destinationRepo ++ "/pulls" 538 | , body = 539 | Http.jsonBody 540 | (Json.Encode.object 541 | [ ( "title", Json.Encode.string params.title ) 542 | , ( "base", encodeBranch params.destinationBranch ) 543 | , ( "head" 544 | , if params.destinationOwner == params.sourceBranchOwner then 545 | encodeBranch params.sourceBranch 546 | 547 | else 548 | ownerToString params.sourceBranchOwner 549 | ++ ":" 550 | ++ branchToString params.sourceBranch 551 | |> Json.Encode.string 552 | ) 553 | , ( "body", Json.Encode.string params.description ) 554 | ] 555 | ) 556 | , resolver = jsonResolver decoder 557 | , timeout = Nothing 558 | } 559 | 560 | 561 | {-| -} 562 | type ContentType 563 | = FileContentType 564 | | DirectoryContentType 565 | | SymLinkType 566 | | SubmoduleType 567 | 568 | 569 | {-| -} 570 | type Content 571 | = FileContent 572 | { encoding : String 573 | , content : String 574 | , sha : ShaHash CommitSha 575 | , downloadUrl : Url 576 | , url : Url 577 | } 578 | | DirectoryContent (List DirectoryEntry) 579 | | Symlink 580 | | Submodule 581 | 582 | 583 | {-| A file directory in a git repo. 584 | -} 585 | type alias DirectoryEntry = 586 | { contentType : ContentType 587 | , name : String 588 | , path : String 589 | , sha : ShaHash CommitSha 590 | , downloadUrl : Maybe Url 591 | , url : Maybe Url 592 | } 593 | 594 | 595 | decodeFile = 596 | Json.Decode.map5 597 | (\encoding content sha_ downloadUrl url -> 598 | { encoding = encoding 599 | , content = content 600 | , sha = sha_ 601 | , downloadUrl = downloadUrl 602 | , url = url 603 | } 604 | ) 605 | (Json.Decode.field "encoding" Json.Decode.string) 606 | (Json.Decode.field "content" Json.Decode.string) 607 | (Json.Decode.field "sha" decodeSha) 608 | (Json.Decode.field "download_url" decodeUrl) 609 | (Json.Decode.field "url" decodeUrl) 610 | 611 | 612 | decodeContentType : Json.Decode.Decoder ContentType 613 | decodeContentType = 614 | Json.Decode.string 615 | |> Json.Decode.andThen 616 | (\text -> 617 | case text of 618 | "file" -> 619 | Json.Decode.succeed FileContentType 620 | 621 | "dir" -> 622 | Json.Decode.succeed DirectoryContentType 623 | 624 | "symlink" -> 625 | Json.Decode.succeed SymLinkType 626 | 627 | "submodule" -> 628 | Json.Decode.succeed SubmoduleType 629 | 630 | _ -> 631 | Json.Decode.fail ("Invalid content type: " ++ text) 632 | ) 633 | 634 | 635 | decodeUrl : Json.Decode.Decoder Url 636 | decodeUrl = 637 | Json.Decode.string 638 | |> Json.Decode.andThen 639 | (\text -> 640 | case Url.fromString text of 641 | Just url -> 642 | Json.Decode.succeed url 643 | 644 | Nothing -> 645 | Json.Decode.fail ("Invalid url: " ++ text) 646 | ) 647 | 648 | 649 | decodeDirectoryEntry : Json.Decode.Decoder DirectoryEntry 650 | decodeDirectoryEntry = 651 | Json.Decode.map6 DirectoryEntry 652 | (Json.Decode.field "type" decodeContentType) 653 | (Json.Decode.field "name" Json.Decode.string) 654 | (Json.Decode.field "path" Json.Decode.string) 655 | (Json.Decode.field "sha" decodeSha) 656 | (Json.Decode.field "download_url" (Json.Decode.nullable decodeUrl)) 657 | (Json.Decode.field "url" (Json.Decode.nullable decodeUrl)) 658 | 659 | 660 | {-| See 661 | 662 | NOTE: Not all input options and output fields are supported yet. Pull requests adding more complete support are welcome. 663 | 664 | -} 665 | getContents : 666 | { authToken : OAuthToken 667 | , owner : Owner 668 | , repo : String 669 | , ref : Maybe String 670 | , path : String 671 | } 672 | -> Task Http.Error Content 673 | getContents params = 674 | let 675 | decoder = 676 | Json.Decode.oneOf 677 | [ Json.Decode.field "type" decodeContentType 678 | |> Json.Decode.andThen 679 | (\contentType -> 680 | case contentType of 681 | FileContentType -> 682 | Json.Decode.map FileContent decodeFile 683 | 684 | DirectoryContentType -> 685 | Json.Decode.map (List.singleton >> DirectoryContent) decodeDirectoryEntry 686 | 687 | SymLinkType -> 688 | Json.Decode.succeed Symlink 689 | 690 | SubmoduleType -> 691 | Json.Decode.succeed Submodule 692 | ) 693 | , Json.Decode.list decodeDirectoryEntry |> Json.Decode.map DirectoryContent 694 | ] 695 | in 696 | Http.task 697 | { method = "GET" 698 | , headers = [ authorizationHeader params.authToken ] 699 | , url = 700 | Url.Builder.crossOrigin githubApiDomain 701 | [ "repos" 702 | , ownerToString params.owner 703 | , params.repo 704 | , "contents" 705 | , params.path 706 | ] 707 | (case params.ref of 708 | Just ref -> 709 | [ Url.Builder.string "ref" ref ] 710 | 711 | Nothing -> 712 | [] 713 | ) 714 | 715 | --"https://api.github.com/repos/" ++ params.repo ++ "/contents/" ++ params.path ++ "?ref=" ++ params.ref 716 | , body = Http.emptyBody 717 | , resolver = jsonResolver decoder 718 | , timeout = Nothing 719 | } 720 | 721 | 722 | {-| See 723 | 724 | NOTE: Not all input options and output fields are supported yet. Pull requests adding more complete support are welcome. 725 | 726 | -} 727 | updateFileContents : 728 | { authToken : OAuthToken 729 | , repo : String 730 | , branch : Branch 731 | , path : String 732 | , sha : ShaHash a 733 | , message : String 734 | , content : String 735 | } 736 | -> 737 | Task 738 | Http.Error 739 | { content : 740 | { sha : ShaHash a 741 | } 742 | } 743 | updateFileContents params = 744 | let 745 | decoder = 746 | Json.Decode.map 747 | (\contentSha -> 748 | { content = { sha = contentSha } } 749 | ) 750 | (Json.Decode.at [ "content", "sha" ] decodeSha) 751 | in 752 | Http.task 753 | { method = "PUT" 754 | , headers = [ authorizationHeader params.authToken ] 755 | , url = "https://api.github.com/repos/" ++ params.repo ++ "/contents/" ++ params.path 756 | , body = 757 | Http.jsonBody 758 | (Json.Encode.object 759 | [ ( "message", Json.Encode.string params.message ) 760 | , ( "content", Json.Encode.string (Base64.encode params.content) ) 761 | , ( "sha", encodeSha params.sha ) 762 | , ( "branch", encodeBranch params.branch ) 763 | ] 764 | ) 765 | , resolver = jsonResolver decoder 766 | , timeout = Nothing 767 | } 768 | 769 | 770 | {-| See 771 | 772 | NOTE: Not all input options and output fields are supported yet. Pull requests adding more complete support are welcome. 773 | 774 | -} 775 | getComments : 776 | { authToken : OAuthToken 777 | , repo : String 778 | , issueNumber : Int 779 | } 780 | -> 781 | Task 782 | Http.Error 783 | (List 784 | { body : String 785 | , user : 786 | { login : String 787 | , avatarUrl : String 788 | } 789 | , createdAt : Time.Posix 790 | , updatedAt : Time.Posix 791 | } 792 | ) 793 | getComments params = 794 | let 795 | decoder = 796 | Json.Decode.map5 797 | (\body userLogin userAvatarUrl createdAt updatedAt -> 798 | { body = body 799 | , user = 800 | { login = userLogin 801 | , avatarUrl = userAvatarUrl 802 | } 803 | , createdAt = createdAt 804 | , updatedAt = updatedAt 805 | } 806 | ) 807 | (Json.Decode.at [ "body" ] Json.Decode.string) 808 | (Json.Decode.at [ "user", "login" ] Json.Decode.string) 809 | (Json.Decode.at [ "user", "avatar_url" ] Json.Decode.string) 810 | (Json.Decode.at [ "created_at" ] Iso8601.decoder) 811 | (Json.Decode.at [ "updated_at" ] Iso8601.decoder) 812 | in 813 | Http.task 814 | { method = "GET" 815 | , headers = [ authorizationHeader params.authToken ] 816 | , url = "https://api.github.com/repos/" ++ params.repo ++ "/issues/" ++ String.fromInt params.issueNumber ++ "/comments" 817 | , body = Http.emptyBody 818 | , resolver = jsonResolver (Json.Decode.list decoder) 819 | , timeout = Nothing 820 | } 821 | 822 | 823 | {-| See 824 | 825 | NOTE: Not all input options and output fields are supported yet. Pull requests adding more complete support are welcome. 826 | 827 | -} 828 | createComment : 829 | { authToken : OAuthToken 830 | , repo : String 831 | , issueNumber : Int 832 | , body : String 833 | } 834 | -> 835 | Task 836 | Http.Error 837 | { body : String 838 | , user : 839 | { login : String 840 | , avatarUrl : String 841 | } 842 | , createdAt : Time.Posix 843 | , updatedAt : Time.Posix 844 | } 845 | createComment params = 846 | let 847 | decoder = 848 | Json.Decode.map5 849 | (\body userLogin userAvatarUrl createdAt updatedAt -> 850 | { body = body 851 | , user = 852 | { login = userLogin 853 | , avatarUrl = userAvatarUrl 854 | } 855 | , createdAt = createdAt 856 | , updatedAt = updatedAt 857 | } 858 | ) 859 | (Json.Decode.at [ "body" ] Json.Decode.string) 860 | (Json.Decode.at [ "user", "login" ] Json.Decode.string) 861 | (Json.Decode.at [ "user", "avatar_url" ] Json.Decode.string) 862 | (Json.Decode.at [ "created_at" ] Iso8601.decoder) 863 | (Json.Decode.at [ "updated_at" ] Iso8601.decoder) 864 | in 865 | Http.task 866 | { method = "POST" 867 | , headers = [ authorizationHeader params.authToken ] 868 | , url = "https://api.github.com/repos/" ++ params.repo ++ "/issues/" ++ String.fromInt params.issueNumber ++ "/comments" 869 | , body = 870 | Http.jsonBody 871 | (Json.Encode.object 872 | [ ( "body", Json.Encode.string params.body ) 873 | ] 874 | ) 875 | , resolver = jsonResolver decoder 876 | , timeout = Nothing 877 | } 878 | 879 | 880 | {-| The the name of owner of a repo, ("avh4" for example) 881 | -} 882 | type Owner 883 | = Owner String 884 | 885 | 886 | {-| -} 887 | owner : String -> Owner 888 | owner = 889 | Owner 890 | 891 | 892 | {-| -} 893 | ownerToString : Owner -> String 894 | ownerToString (Owner owner_) = 895 | owner_ 896 | 897 | 898 | {-| The name of a git branch, i.e. "master", "main", "feature-branch" 899 | -} 900 | type Branch 901 | = Branch String 902 | 903 | 904 | {-| -} 905 | branch : String -> Branch 906 | branch = 907 | Branch 908 | 909 | 910 | {-| -} 911 | branchToString : Branch -> String 912 | branchToString (Branch branch_) = 913 | branch_ 914 | 915 | 916 | encodeBranch : Branch -> Json.Encode.Value 917 | encodeBranch = 918 | branchToString >> Json.Encode.string 919 | 920 | 921 | decodeBranch : Json.Decode.Decoder Branch 922 | decodeBranch = 923 | Json.Decode.map branch Json.Decode.string 924 | 925 | 926 | {-| An OAuth token used to authenticate various Github API requests. Not to be confused with `OAuthCode` which is used in order to generate an `OAuthToken`. 927 | -} 928 | type OAuthToken 929 | = OAuthToken String 930 | 931 | 932 | {-| -} 933 | oauthToken : String -> OAuthToken 934 | oauthToken = 935 | OAuthToken 936 | 937 | 938 | {-| -} 939 | oauthTokenToString : OAuthToken -> String 940 | oauthTokenToString (OAuthToken token) = 941 | token 942 | 943 | 944 | {-| A SHA that's used as a pointer for a tree 945 | -} 946 | type TreeSha 947 | = TreeSha Never 948 | 949 | 950 | {-| A SHA that's used as a pointer for a commit 951 | -} 952 | type CommitSha 953 | = CommitSha Never 954 | 955 | 956 | {-| A SHA identifier 957 | -} 958 | type ShaHash a 959 | = ShaHash String 960 | 961 | 962 | {-| -} 963 | sha : String -> ShaHash a 964 | sha = 965 | ShaHash 966 | 967 | 968 | {-| Get the raw sha string. 969 | -} 970 | shaToString : ShaHash a -> String 971 | shaToString (ShaHash shaHash) = 972 | shaHash 973 | 974 | 975 | decodeSha : Json.Decode.Decoder (ShaHash a) 976 | decodeSha = 977 | Json.Decode.string |> Json.Decode.map sha 978 | 979 | 980 | encodeSha : ShaHash a -> Json.Encode.Value 981 | encodeSha = 982 | shaToString >> Json.Encode.string 983 | 984 | 985 | {-| See 986 | 987 | NOTE: Not all input options and output fields are supported yet. Pull requests adding more complete support are welcome. 988 | 989 | -} 990 | createFork : 991 | { authToken : OAuthToken 992 | , owner : Owner 993 | , repo : String 994 | } 995 | -> 996 | Task 997 | Http.Error 998 | { owner : Owner 999 | , repo : String 1000 | } 1001 | createFork params = 1002 | let 1003 | decoder = 1004 | Json.Decode.map2 1005 | (\owner_ repo -> 1006 | { owner = owner owner_ 1007 | , repo = repo 1008 | } 1009 | ) 1010 | (Json.Decode.at [ "owner", "login" ] Json.Decode.string) 1011 | (Json.Decode.at [ "name" ] Json.Decode.string) 1012 | in 1013 | Http.task 1014 | { method = "POST" 1015 | , headers = [ authorizationHeader params.authToken ] 1016 | , url = "https://api.github.com/repos/" ++ ownerToString params.owner ++ "/" ++ params.repo ++ "/forks" 1017 | , body = Http.emptyBody 1018 | , resolver = jsonResolver decoder 1019 | , timeout = Nothing 1020 | } 1021 | 1022 | 1023 | authorizationHeader : OAuthToken -> Http.Header 1024 | authorizationHeader (OAuthToken authToken_) = 1025 | Http.header "Authorization" ("token " ++ authToken_) 1026 | 1027 | 1028 | jsonResolver : Json.Decode.Decoder a -> Http.Resolver Http.Error a 1029 | jsonResolver decoder = 1030 | Http.stringResolver <| 1031 | \response -> 1032 | case response of 1033 | Http.GoodStatus_ _ body -> 1034 | Json.Decode.decodeString decoder body 1035 | |> Result.mapError Json.Decode.errorToString 1036 | |> Result.mapError Http.BadBody 1037 | 1038 | Http.BadUrl_ message -> 1039 | Err (Http.BadUrl message) 1040 | 1041 | Http.Timeout_ -> 1042 | Err Http.Timeout 1043 | 1044 | Http.NetworkError_ -> 1045 | Err Http.NetworkError 1046 | 1047 | Http.BadStatus_ metadata _ -> 1048 | Err (Http.BadStatus metadata.statusCode) 1049 | 1050 | 1051 | bytesResolver : Http.Resolver Http.Error Bytes 1052 | bytesResolver = 1053 | Http.bytesResolver <| 1054 | \response -> 1055 | case response of 1056 | Http.GoodStatus_ _ body -> 1057 | Ok body 1058 | 1059 | Http.BadUrl_ message -> 1060 | Err (Http.BadUrl message) 1061 | 1062 | Http.Timeout_ -> 1063 | Err Http.Timeout 1064 | 1065 | Http.NetworkError_ -> 1066 | Err Http.NetworkError 1067 | 1068 | Http.BadStatus_ metadata _ -> 1069 | Err (Http.BadStatus metadata.statusCode) 1070 | 1071 | 1072 | 1073 | ---- New stuff ---- 1074 | 1075 | 1076 | {-| Github application client id 1077 | -} 1078 | type ClientId 1079 | = ClientId String 1080 | 1081 | 1082 | {-| -} 1083 | clientId : String -> ClientId 1084 | clientId = 1085 | ClientId 1086 | 1087 | 1088 | {-| -} 1089 | clientIdToString : ClientId -> String 1090 | clientIdToString (ClientId a) = 1091 | a 1092 | 1093 | 1094 | {-| Github application client secret (do not include this on your frontend!) 1095 | -} 1096 | type ClientSecret 1097 | = ClientSecret String 1098 | 1099 | 1100 | {-| -} 1101 | clientSecret : String -> ClientSecret 1102 | clientSecret = 1103 | ClientSecret 1104 | 1105 | 1106 | {-| -} 1107 | clientSecretToString : ClientSecret -> String 1108 | clientSecretToString (ClientSecret a) = 1109 | a 1110 | 1111 | 1112 | {-| Not to be confused with `OAuthToken`! This is an intermediate value you get while generating an `OAuthToken`. 1113 | -} 1114 | type OAuthCode 1115 | = OAuthCode String 1116 | 1117 | 1118 | {-| -} 1119 | oauthCode : String -> OAuthCode 1120 | oauthCode = 1121 | OAuthCode 1122 | 1123 | 1124 | {-| -} 1125 | oauthCodeToString : OAuthCode -> String 1126 | oauthCodeToString (OAuthCode a) = 1127 | a 1128 | 1129 | 1130 | githubDomain : String 1131 | githubDomain = 1132 | "https://github.com" 1133 | 1134 | 1135 | githubApiDomain : String 1136 | githubApiDomain = 1137 | "https://api.github.com" 1138 | 1139 | 1140 | {-| See 1141 | -} 1142 | type Scope 1143 | = RepoScope 1144 | | RepoStatusScope 1145 | | RepoDeploymentScope 1146 | | PublicRepoScope 1147 | | RepoInviteScope 1148 | 1149 | 1150 | {-| -} 1151 | scopeToString : Scope -> String 1152 | scopeToString scope = 1153 | case scope of 1154 | RepoScope -> 1155 | "repo" 1156 | 1157 | RepoStatusScope -> 1158 | "repo:status" 1159 | 1160 | RepoDeploymentScope -> 1161 | "repo_deployment" 1162 | 1163 | PublicRepoScope -> 1164 | "public_repo" 1165 | 1166 | RepoInviteScope -> 1167 | "repo:invite" 1168 | 1169 | 1170 | {-| -} 1171 | scopeFromString : String -> Maybe Scope 1172 | scopeFromString string = 1173 | case string of 1174 | "repo" -> 1175 | Just RepoScope 1176 | 1177 | "repo:status" -> 1178 | Just RepoStatusScope 1179 | 1180 | "repo_deployment" -> 1181 | Just RepoDeploymentScope 1182 | 1183 | "public_repo" -> 1184 | Just PublicRepoScope 1185 | 1186 | "repo:invite" -> 1187 | Just RepoInviteScope 1188 | 1189 | _ -> 1190 | Nothing 1191 | 1192 | 1193 | {-| The link a user clicks on to be prompted about authorizing a github app. 1194 | See 1195 | -} 1196 | oauthLink : { clientId : ClientId, redirectUri : Maybe String, scopes : List Scope, state : Maybe String } -> String 1197 | oauthLink params = 1198 | Url.Builder.crossOrigin 1199 | githubDomain 1200 | [ "login", "oauth", "authorize" ] 1201 | (Url.Builder.string "client_id" (clientIdToString params.clientId) 1202 | :: Url.Builder.string 1203 | "scope" 1204 | (List.map scopeToString params.scopes |> String.join " ") 1205 | :: (case params.state of 1206 | Just state -> 1207 | [ Url.Builder.string "state" state ] 1208 | 1209 | Nothing -> 1210 | [] 1211 | ) 1212 | ++ (case params.redirectUri of 1213 | Just redirectUri -> 1214 | [ Url.Builder.string "redirect_uri" redirectUri ] 1215 | 1216 | Nothing -> 1217 | [] 1218 | ) 1219 | ) 1220 | 1221 | 1222 | {-| See 1223 | -} 1224 | getAccessToken : 1225 | { clientId : ClientId 1226 | , clientSecret : ClientSecret 1227 | , oauthCode : OAuthCode 1228 | , state : Maybe String 1229 | } 1230 | -> Task Http.Error AccessTokenResponse 1231 | getAccessToken params = 1232 | Http.task 1233 | { method = "POST" 1234 | , headers = [] 1235 | , url = 1236 | Url.Builder.crossOrigin 1237 | githubDomain 1238 | [ "login", "oauth", "access_token" ] 1239 | (Url.Builder.string "client_id" (clientIdToString params.clientId) 1240 | :: Url.Builder.string "client_secret" (clientSecretToString params.clientSecret) 1241 | :: Url.Builder.string "code" (oauthCodeToString params.oauthCode) 1242 | :: (case params.state of 1243 | Just state -> 1244 | [ Url.Builder.string "state" state ] 1245 | 1246 | Nothing -> 1247 | [] 1248 | ) 1249 | ) 1250 | , body = Http.emptyBody 1251 | , resolver = 1252 | Http.stringResolver <| 1253 | \response -> 1254 | case response of 1255 | Http.GoodStatus_ _ body -> 1256 | let 1257 | parameters = 1258 | String.split "&" body 1259 | |> List.filterMap 1260 | (\parameter -> 1261 | case String.split "=" parameter of 1262 | name :: value :: [] -> 1263 | Just ( name, value ) 1264 | 1265 | _ -> 1266 | Nothing 1267 | ) 1268 | |> Dict.fromList 1269 | 1270 | result = 1271 | Maybe.map3 AccessTokenResponse 1272 | (Dict.get "access_token" parameters |> Maybe.map oauthToken) 1273 | (Dict.get "scope" parameters) 1274 | (Dict.get "token_type" parameters) 1275 | in 1276 | case result of 1277 | Just good -> 1278 | Ok good 1279 | 1280 | Nothing -> 1281 | ("Failed to parse parameters from body: " ++ body) 1282 | |> Http.BadBody 1283 | |> Err 1284 | 1285 | Http.BadUrl_ message -> 1286 | Err (Http.BadUrl message) 1287 | 1288 | Http.Timeout_ -> 1289 | Err Http.Timeout 1290 | 1291 | Http.NetworkError_ -> 1292 | Err Http.NetworkError 1293 | 1294 | Http.BadStatus_ metadata _ -> 1295 | Err (Http.BadStatus metadata.statusCode) 1296 | , timeout = Nothing 1297 | } 1298 | 1299 | 1300 | {-| -} 1301 | type alias AccessTokenResponse = 1302 | { accessToken : OAuthToken 1303 | , scope : String 1304 | , tokenType : String 1305 | } 1306 | 1307 | 1308 | {-| See 1309 | -} 1310 | createIssue : { authToken : OAuthToken, owner : Owner, repo : String, title : NonemptyString, body : String } -> Task Http.Error () 1311 | createIssue params = 1312 | Http.task 1313 | { method = "POST" 1314 | , headers = [ authorizationHeader params.authToken ] 1315 | , url = 1316 | Url.Builder.crossOrigin githubApiDomain 1317 | [ "repos", ownerToString params.owner, params.repo, "issues" ] 1318 | [] 1319 | , body = 1320 | Http.jsonBody 1321 | (Json.Encode.object 1322 | [ ( "title", Json.Encode.string (String.Nonempty.toString params.title) ) 1323 | , ( "body", Json.Encode.string params.body ) 1324 | ] 1325 | ) 1326 | , resolver = jsonResolver (Json.Decode.succeed ()) 1327 | , timeout = Nothing 1328 | } 1329 | --------------------------------------------------------------------------------