├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── .swift-version ├── .swiftlint.yml ├── Cartfile ├── Cartfile.resolved ├── LICENSE.md ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── Tentacle │ ├── Author.swift │ ├── Availability.swift │ ├── Branch.swift │ ├── Client.swift │ ├── Color.swift │ ├── Comment.swift │ ├── Commit.swift │ ├── Content.swift │ ├── File.swift │ ├── FileResponse.swift │ ├── FoundationExtensions.swift │ ├── GitHubError.swift │ ├── Identifiable.swift │ ├── Info.plist │ ├── Issue.swift │ ├── JSONExtensions.swift │ ├── Label.swift │ ├── Milestone.swift │ ├── Organization.swift │ ├── PullRequest.swift │ ├── Release.swift │ ├── Repository.swift │ ├── RepositoryInfo.swift │ ├── Request.swift │ ├── ResourceType.swift │ ├── Response.swift │ ├── Server.swift │ ├── Sha.swift │ ├── Tentacle.h │ ├── Tree.swift │ └── User.swift ├── Tentacle.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── Tentacle-OSX.xcscheme │ ├── Tentacle-iOS.xcscheme │ └── update-test-fixtures.xcscheme ├── Tentacle.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── WorkspaceSettings.xcsettings ├── Tests └── TentacleTests │ ├── BranchTests.swift │ ├── ClientTests.swift │ ├── ColorTests.swift │ ├── CommentsTests.swift │ ├── ContentTests.swift │ ├── EndpointTests.swift │ ├── FileResponseTests.swift │ ├── FileTests.swift │ ├── Fixture.swift │ ├── Fixtures │ ├── create-file-sample-response.data │ ├── orgs-raccommunity-repos.data │ ├── orgs-raccommunity-repos.response │ ├── repos-Carthage-Carthage-releases-tags-0.15.data │ ├── repos-Carthage-Carthage-releases-tags-0.15.response │ ├── repos-Carthage-Carthage-releases.page-1-per_page-30.data │ ├── repos-Carthage-Carthage-releases.page-1-per_page-30.response │ ├── repos-Carthage-Carthage-releases.page-2-per_page-30.data │ ├── repos-Carthage-Carthage-releases.page-2-per_page-30.response │ ├── repos-Carthage-Carthage-releases.page-3-per_page-30.data │ ├── repos-Carthage-Carthage-releases.page-3-per_page-30.response │ ├── repos-Carthage-ReactiveTask-branches.data │ ├── repos-Carthage-ReactiveTask-branches.response │ ├── repos-Palleas-Opensource-Sample-repository-issues-1-comments.data │ ├── repos-Palleas-Opensource-Sample-repository-issues-1-comments.response │ ├── repos-Palleas-opensource-Sample-repository-contents-README.md.data │ ├── repos-Palleas-opensource-Sample-repository-contents-README.md.response │ ├── repos-Palleas-opensource-Sample-repository-contents-Tools-say.data │ ├── repos-Palleas-opensource-Sample-repository-contents-Tools-say.response │ ├── repos-Palleas-opensource-Sample-repository-contents-Tools.data │ ├── repos-Palleas-opensource-Sample-repository-contents-Tools.response │ ├── repos-Palleas-opensource-Sample-repository-contents-say.data │ ├── repos-Palleas-opensource-Sample-repository-contents-say.response │ ├── repos-Palleas-opensource-Sample-repository-git-trees-0c0dfafa361836e11aedcbb95c1f05d3f654aef0.data │ ├── repos-Palleas-opensource-Sample-repository-git-trees-0c0dfafa361836e11aedcbb95c1f05d3f654aef0.response │ ├── repos-Palleas-opensource-Sample-repository-issues-1.data │ ├── repos-Palleas-opensource-Sample-repository-issues-1.response │ ├── repos-Palleas-opensource-Sample-repository-issues.data │ ├── repos-Palleas-opensource-Sample-repository-issues.response │ ├── repos-mdiep-MDPSplitView-releases-assets-433845.data │ ├── repos-mdiep-MDPSplitView-releases-assets-433845.response │ ├── repos-mdiep-MDPSplitView-releases-latest.data │ ├── repos-mdiep-MDPSplitView-releases-latest.response │ ├── repos-mdiep-MDPSplitView-releases-tags-1.0.2.data │ ├── repos-mdiep-MDPSplitView-releases-tags-1.0.2.response │ ├── repos-mdiep-NonExistent-releases-tags-tag.data │ ├── repos-mdiep-NonExistent-releases-tags-tag.response │ ├── repos-mdiep-Tentacle-contents-Carthage-Checkouts-ReactiveSwift.data │ ├── repos-mdiep-Tentacle-contents-Carthage-Checkouts-ReactiveSwift.response │ ├── repos-mdiep-Tentacle-contents-update-test-fixtures.data │ ├── repos-mdiep-Tentacle-contents-update-test-fixtures.response │ ├── repos-mdiep-Tentacle.data │ ├── repos-mdiep-Tentacle.response │ ├── repos-torvalds-linux-releases-tags-v4.4.data │ ├── repos-torvalds-linux-releases-tags-v4.4.response │ ├── users-Palleas-Opensource-repos.data │ ├── users-Palleas-Opensource-repos.response │ ├── users-mdiep.data │ ├── users-mdiep.response │ ├── users-test.data │ └── users-test.response │ ├── GitHubErrorTests.swift │ ├── HTTPStub.swift │ ├── Info.plist │ ├── IssuesTests.swift │ ├── ReleaseTests.swift │ ├── RepositoryInfoTests.swift │ ├── RepositoryTests.swift │ ├── ResponseTests.swift │ ├── ServerTests.swift │ ├── TreeTests.swift │ └── UserTests.swift ├── script └── cibuild └── update-test-fixtures ├── Info.plist └── main.swift /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | - main 6 | pull_request: 7 | jobs: 8 | xcode: 9 | name: Xcode - ${{ matrix.scheme }} 10 | runs-on: macos-latest 11 | strategy: 12 | matrix: 13 | scheme: 14 | - Tentacle-iOS 15 | - Tentacle-OSX 16 | steps: 17 | - uses: actions/checkout@v3 18 | with: 19 | submodules: recursive 20 | - name: Build 21 | run: script/cibuild Tentacle.xcworkspace ${{ matrix.scheme }} build-for-testing 22 | - name: Test 23 | run: script/cibuild Tentacle.xcworkspace ${{ matrix.scheme }} test-without-building 24 | 25 | spm: 26 | name: SwiftPM 27 | runs-on: macos-latest 28 | steps: 29 | - uses: actions/checkout@v3 30 | with: 31 | submodules: recursive 32 | - name: Build 33 | run: swift build --build-tests 34 | - name: Test 35 | run: swift test --skip-build 36 | 37 | update-test-fixures: 38 | name: Update Test Fixtures 39 | runs-on: macos-latest 40 | steps: 41 | - uses: actions/checkout@v3 42 | with: 43 | submodules: recursive 44 | - name: Build 45 | run: script/cibuild Tentacle.xcworkspace update-test-fixtures build 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | .swiftpm/xcode 40 | 41 | # CocoaPods 42 | # 43 | # We recommend against adding the Pods directory to your .gitignore. However 44 | # you should judge for yourself, the pros and cons are mentioned at: 45 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 46 | # 47 | # Pods/ 48 | 49 | # Carthage 50 | # 51 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 52 | # Carthage/Checkouts 53 | 54 | Carthage/Build 55 | 56 | # fastlane 57 | # 58 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 59 | # screenshots whenever they are needed. 60 | # For more information about the recommended setup visit: 61 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 62 | 63 | fastlane/report.xml 64 | fastlane/screenshots 65 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Carthage/Checkouts/ReactiveSwift"] 2 | path = Carthage/Checkouts/ReactiveSwift 3 | url = https://github.com/ReactiveCocoa/ReactiveSwift.git 4 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.7.1 -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Sources 3 | whitelist_rules: 4 | - trailing_closure 5 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "ReactiveCocoa/ReactiveSwift" ~> 7.1 2 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "ReactiveCocoa/ReactiveSwift" "7.1.1" 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Matt Diephouse 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. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "reactiveswift", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/ReactiveCocoa/ReactiveSwift.git", 7 | "state" : { 8 | "revision" : "40c465af19b993344e84355c00669ba2022ca3cd", 9 | "version" : "7.1.1" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7.1 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "Tentacle", 6 | products: [ 7 | .library(name: "Tentacle", targets: ["Tentacle"]), 8 | ], 9 | dependencies: [ 10 | .package(url: "https://github.com/ReactiveCocoa/ReactiveSwift.git", from: "7.1.1"), 11 | ], 12 | targets: [ 13 | .target(name: "Tentacle", dependencies: ["ReactiveSwift"]), 14 | .testTarget( 15 | name: "TentacleTests", 16 | dependencies: ["Tentacle"], 17 | resources: [ 18 | .copy("Fixtures"), 19 | ] 20 | ), 21 | ], 22 | swiftLanguageVersions: [.v5] 23 | ) 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tentacle [![MIT license](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://raw.githubusercontent.com/mdiep/Tentacle/master/LICENSE.md) 2 | A Swift framework for the GitHub API 3 | 4 | ```swift 5 | let client = Client(.dotCom, token: "…") 6 | let repo = Repository(owner: "ReactiveCocoa", name: "ReactiveCocoa") 7 | let request = repo.release(forTag: "tag-name") 8 | client 9 | .execute(request) 10 | .startWithResult { result in 11 | switch result { 12 | case let .success(response, release): 13 | print("Downloaded release: \(release)") 14 | case let .failure(error): 15 | print("An error occurred: \(error)") 16 | } 17 | } 18 | ``` 19 | 20 | Tentacle is built with [ReactiveSwift](https://github.com/ReactiveCocoa/ReactiveSwift). 21 | 22 | ## License 23 | Tentacle is available under the [MIT License](LICENSE.md) 24 | -------------------------------------------------------------------------------- /Sources/Tentacle/Author.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Author.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2016-12-22. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Author: ResourceType, Encodable { 12 | /// Name of the Author 13 | let name: String 14 | /// Email of the Author 15 | let email: String 16 | 17 | public init(name: String, email: String) { 18 | self.name = name 19 | self.email = email 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Tentacle/Availability.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Availability.swift 3 | // Tentacle 4 | // 5 | // Created by Syo Ikeda on 11/6/16. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveSwift 11 | -------------------------------------------------------------------------------- /Sources/Tentacle/Branch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Branch.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2017-02-15. 6 | // Copyright © 2017 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Repository { 12 | /// A request for the branches in the repository. 13 | /// 14 | /// https://developer.github.com/v3/repos/branches/#list-branches 15 | public var branches: Request<[Branch]> { 16 | return Request(method: .get, path: "/repos/\(owner)/\(name)/branches") 17 | } 18 | } 19 | 20 | public struct Branch: ResourceType { 21 | 22 | public struct Commit: ResourceType { 23 | public let sha: SHA 24 | } 25 | 26 | /// Name of the branch 27 | public let name: String 28 | 29 | /// The commit the branch points to 30 | public let commit: Commit 31 | 32 | public init(name: String, commit: Commit) { 33 | self.name = name 34 | self.commit = commit 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Tentacle/Color.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2016-07-19. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | #if os(iOS) || os(tvOS) 12 | import UIKit 13 | public typealias Color = UIColor 14 | #else 15 | import Cocoa 16 | public typealias Color = NSColor 17 | #endif 18 | 19 | extension Color { 20 | internal convenience init(hex: String) { 21 | precondition(hex.count == 6) 22 | 23 | let scanner = Scanner(string: hex) 24 | var rgb: UInt64 = 0 25 | scanner.scanHexInt64(&rgb) 26 | 27 | let r = CGFloat((rgb & 0xff0000) >> 16) / 255.0 28 | let g = CGFloat((rgb & 0x00ff00) >> 8) / 255.0 29 | let b = CGFloat((rgb & 0x0000ff) >> 0) / 255.0 30 | self.init(red: r, green: g, blue: b, alpha: 1) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Tentacle/Comment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Comment.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2016-07-27. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Repository { 12 | /// A request for the comments on the given issue. 13 | /// 14 | /// https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue 15 | public func comments(onIssue issue: Int) -> Request<[Comment]> { 16 | return Request(method: .get, path: "/repos/\(owner)/\(name)/issues/\(issue)/comments") 17 | } 18 | } 19 | 20 | public struct Comment: CustomStringConvertible, ResourceType, Identifiable { 21 | 22 | /// The id of the issue 23 | public let id: ID 24 | /// The URL to view this comment in a browser 25 | public let url: URL 26 | /// The date this comment was created at 27 | public let createdAt: Date 28 | /// The date this comment was last updated at 29 | public let updatedAt: Date 30 | /// The body of the comment 31 | public let body: String 32 | /// The author of this comment 33 | public let author: UserInfo 34 | 35 | public var description: String { 36 | return body 37 | } 38 | 39 | private enum CodingKeys: String, CodingKey { 40 | case id 41 | case url = "html_url" 42 | case createdAt = "created_at" 43 | case updatedAt = "updated_at" 44 | case body 45 | case author = "user" 46 | 47 | 48 | } 49 | } 50 | 51 | extension Comment: Equatable { 52 | public static func ==(lhs: Comment, rhs: Comment) -> Bool { 53 | return lhs.id == rhs.id 54 | && lhs.url == rhs.url 55 | && lhs.body == rhs.body 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/Tentacle/Commit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Commit.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2016-12-22. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Commit: ResourceType { 12 | /// SHA of the commit 13 | public let sha: SHA 14 | 15 | /// Author of the commit 16 | public let author: Author 17 | 18 | /// Committer of the commit 19 | public let committer: Author 20 | 21 | /// Comit Message 22 | public let message: String 23 | 24 | /// URL to see the commit in a browser 25 | public let url: URL 26 | 27 | /// Parents commits 28 | public let parents: [Parent] 29 | 30 | public struct Parent: ResourceType { 31 | /// URL to see the parent commit in a browser 32 | public let url: URL 33 | 34 | /// SHA of the parent commit 35 | public let sha: SHA 36 | } 37 | 38 | public struct Author: ResourceType { 39 | /// Date the author made the commit 40 | public let date: Date 41 | /// Name of the author 42 | public let name: String 43 | /// Email of the author 44 | public let email: String 45 | } 46 | } 47 | 48 | extension Commit: Hashable { 49 | public func hash(into hasher: inout Hasher) { 50 | sha.hash(into: &hasher) 51 | } 52 | 53 | public static func ==(lhs: Commit, rhs: Commit) -> Bool { 54 | return lhs.sha == rhs.sha 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/Tentacle/Content.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Content.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2016-11-28. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Repository { 12 | /// A request for the content at a given path in the repository. 13 | /// 14 | /// https://developer.github.com/v3/repos/contents/#get-contents 15 | public func content(atPath path: String, atRef ref: String? = nil) -> Request { 16 | let queryItems: [URLQueryItem] 17 | if let ref = ref { 18 | queryItems = [ URLQueryItem(name: "ref", value: ref) ] 19 | } else { 20 | queryItems = [] 21 | } 22 | return Request(method: .get, path: "/repos/\(owner)/\(name)/contents/\(path)", queryItems: queryItems) 23 | } 24 | } 25 | 26 | /// Content 27 | /// https://developer.github.com/v3/repos/contents/ 28 | /// 29 | /// - file: a file when queried directly in a repository 30 | /// - directory: a directory when queried directly in a repository (may contain multiple files) 31 | public enum Content: ResourceType, Hashable { 32 | /// A file in a repository 33 | public struct File: CustomStringConvertible, ResourceType { 34 | 35 | public enum ContentTypeName: String, Decodable { 36 | case file 37 | case directory = "dir" 38 | case symlink 39 | case submodule 40 | } 41 | 42 | /// Type of content in a repository 43 | /// 44 | /// - file: a file in a repository 45 | /// - directory: a directory in a repository 46 | /// - symlink: a symlink in a repository not targeting a file inside the same repository 47 | /// - submodule: a submodule in a repository 48 | public enum ContentType: Decodable, Equatable { 49 | /// A file a in a repository 50 | case file(size: Int, downloadURL: URL?) 51 | 52 | /// A directory in a repository 53 | case directory 54 | 55 | /// A symlink in a repository. Target and URL are optional because they are treated as regular files 56 | /// when they are the result of a query for a directory 57 | /// See https://developer.github.com/v3/repos/contents/ 58 | case symlink(target: String?, downloadURL: URL?) 59 | 60 | /// A submodule in a repository. URL is optional because they are treated as regular files 61 | /// when they are the result of a query for a directory 62 | /// See https://developer.github.com/v3/repos/contents/ 63 | case submodule(url: String?) 64 | 65 | public init(from decoder: Decoder) throws { 66 | let container = try decoder.container(keyedBy: CodingKeys.self) 67 | let type = try container.decode(ContentTypeName.self, forKey: .type) 68 | switch type { 69 | case .file: 70 | let size = try container.decode(Int.self, forKey: .size) 71 | if let url = try container.decodeIfPresent(URL.self, forKey: .downloadURL) { 72 | self = .file(size: size, downloadURL: url) 73 | } else { 74 | self = .submodule(url: nil) 75 | } 76 | case .directory: 77 | self = .directory 78 | case .submodule: 79 | let url = try container.decodeIfPresent(String.self, forKey: .submoduleURL) 80 | self = .submodule(url: url) 81 | case .symlink: 82 | let target = try container.decodeIfPresent(String.self, forKey: .target) 83 | let url = try container.decodeIfPresent(URL.self, forKey: .downloadURL) 84 | self = .symlink(target: target, downloadURL: url) 85 | } 86 | } 87 | 88 | private enum CodingKeys: String, CodingKey { 89 | case type 90 | case size 91 | case target 92 | case downloadURL = "download_url" 93 | case submoduleURL = "submodule_git_url" 94 | } 95 | } 96 | 97 | /// The type of content 98 | public let content: ContentType 99 | 100 | /// Name of the file 101 | public let name: String 102 | 103 | /// Path to the file in repository 104 | public let path: String 105 | 106 | /// Sha of the file 107 | public let sha: String 108 | 109 | /// URL to preview the content 110 | public let url: URL 111 | 112 | public var description: String { 113 | return name 114 | } 115 | 116 | public init(from decoder: Decoder) throws { 117 | let container = try decoder.container(keyedBy: CodingKeys.self) 118 | 119 | self.name = try container.decode(String.self, forKey: .name) 120 | self.path = try container.decode(String.self, forKey: .path) 121 | self.sha = try container.decode(String.self, forKey: .sha) 122 | self.url = try container.decode(URL.self, forKey: .url) 123 | self.content = try ContentType(from: decoder) 124 | } 125 | 126 | public init(content: ContentType, name: String, path: String, sha: String, url: URL) { 127 | self.name = name 128 | self.path = path 129 | self.sha = sha 130 | self.url = url 131 | self.content = content 132 | } 133 | 134 | private enum CodingKeys: String, CodingKey { 135 | case name 136 | case path 137 | case sha 138 | case url = "html_url" 139 | case content 140 | } 141 | 142 | public func hash(into hasher: inout Hasher) { 143 | name.hash(into: &hasher) 144 | } 145 | } 146 | 147 | case file(File) 148 | case directory([File]) 149 | 150 | public init(from decoder: Decoder) throws { 151 | 152 | do { 153 | let file = try File(from: decoder) 154 | self = .file(file) 155 | } catch { 156 | var container = try decoder.unkeyedContainer() 157 | var files = [File]() 158 | while !container.isAtEnd { 159 | files.append(try container.decode(File.self)) 160 | } 161 | 162 | self = .directory(files) 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /Sources/Tentacle/File.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2016-12-21. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Repository { 12 | /// A request to create a file at a given path in the repository. 13 | /// 14 | /// https://developer.github.com/v3/repos/contents/#create-a-file 15 | public func create(file: File, atPath path: String, inBranch branch: String? = nil) -> Request { 16 | let queryItems: [URLQueryItem] 17 | if let branch = branch { 18 | queryItems = [ URLQueryItem(name: "branch", value: branch) ] 19 | } else { 20 | queryItems = [] 21 | } 22 | return Request( 23 | method: .put, 24 | path: "/repos/\(owner)/\(name)/contents/\(path)", 25 | queryItems: queryItems 26 | ) 27 | } 28 | } 29 | 30 | public struct File: ResourceType, Encodable { 31 | /// Commit message 32 | public let message: String 33 | /// The committer of the commit 34 | public let committer: Author? 35 | /// The author of the commit 36 | public let author: Author? 37 | /// Content of the file to create 38 | public let content: Data 39 | /// Branch in which the file will be created 40 | public let branch: String? 41 | 42 | public init(message: String, committer: Author?, author: Author?, content: Data, branch: String?) { 43 | self.message = message 44 | self.committer = committer 45 | self.author = author 46 | self.content = content 47 | self.branch = branch 48 | } 49 | 50 | public func encode(to encoder: Encoder) throws { 51 | var container = encoder.container(keyedBy: CodingKeys.self) 52 | try container.encode(message, forKey: .message) 53 | try container.encode(committer, forKey: .committer) 54 | try container.encode(author, forKey: .author) 55 | try container.encode(content.base64EncodedString(), forKey: .content) 56 | try container.encode(branch, forKey: .branch) 57 | } 58 | 59 | public func hash(into hasher: inout Hasher) { 60 | message.hash(into: &hasher) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/Tentacle/FileResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileResponse.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2016-12-22. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct FileResponse: ResourceType { 12 | /// Created file 13 | public let content: Content 14 | 15 | /// Commit associated with the file that was created 16 | public let commit: Commit 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Tentacle/FoundationExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FoundationExtensions.swift 3 | // Tentacle 4 | // 5 | // Created by Matt Diephouse on 4/12/16. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension DateFormatter { 12 | @nonobjc public static var iso8601: DateFormatter = { 13 | let formatter = DateFormatter() 14 | formatter.locale = Locale(identifier:"en_US_POSIX") 15 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" 16 | formatter.timeZone = TimeZone(abbreviation:"UTC") 17 | return formatter 18 | }() 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Tentacle/GitHubError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitHubError.swift 3 | // Tentacle 4 | // 5 | // Created by Matt Diephouse on 3/4/16. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An error from the GitHub API. 12 | public struct GitHubError: CustomStringConvertible, Error, ResourceType { 13 | /// The error message from the API. 14 | public let message: String 15 | 16 | public var description: String { 17 | return message 18 | } 19 | 20 | public init(message: String) { 21 | self.message = message 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Tentacle/Identifiable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Identifiable.swift 3 | // Tentacle-OSX 4 | // 5 | // Created by Romain Pouclet on 2017-06-18. 6 | // Copyright © 2017 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol Identifiable: Hashable { 12 | var id: ID { get } 13 | } 14 | 15 | extension Identifiable { 16 | public func hash(into hasher: inout Hasher) { 17 | id.hash(into: &hasher) 18 | } 19 | } 20 | 21 | public struct ID: Decodable, Hashable { 22 | var rawValue: Int 23 | 24 | public var string: String { 25 | return "\(rawValue)" 26 | } 27 | 28 | public init(from decoder: Decoder) throws { 29 | let container = try decoder.singleValueContainer() 30 | rawValue = try container.decode(Int.self) 31 | } 32 | 33 | } 34 | 35 | extension ID: ExpressibleByIntegerLiteral { 36 | public init(integerLiteral value: Int) { 37 | self.rawValue = value 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Tentacle/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2016 Matt Diephouse. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Sources/Tentacle/Issue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Issue.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2016-05-23. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Repository { 12 | /// A request for issues in the repository. 13 | /// 14 | /// https://developer.github.com/v3/issues/#list-issues-for-a-repository 15 | public var issues: Request<[Issue]> { 16 | return Request(method: .get, path: "/repos/\(owner)/\(name)/issues") 17 | } 18 | 19 | /// A request for an issue in the repository 20 | /// 21 | /// https://developer.github.com/v3/issues/#get-a-single-issue 22 | public func issue(id: ID) -> Request { 23 | return Request(method: .get, path: "/repos/\(owner)/\(name)/issues/\(id.string)") 24 | } 25 | } 26 | 27 | /// An Issue on Github 28 | public struct Issue: CustomStringConvertible, ResourceType, Identifiable { 29 | public enum State: String, ResourceType { 30 | case open = "open" 31 | case closed = "closed" 32 | } 33 | 34 | /// The id of the issue 35 | public let id: ID 36 | 37 | /// The URL to view this issue in a browser 38 | public let url: URL? 39 | 40 | /// The number of the issue in the repository it belongs to 41 | public let number: Int 42 | 43 | /// The state of the issue, open or closed 44 | public let state: State 45 | 46 | /// The title of the issue 47 | public let title: String 48 | 49 | /// The body of the issue 50 | public let body: String 51 | 52 | /// The author of the issue 53 | public let user: UserInfo? 54 | 55 | /// The labels associated to this issue, if any 56 | public let labels: [Label] 57 | 58 | /// The user assigned to this issue, if any 59 | public let assignees: [UserInfo] 60 | 61 | /// The milestone this issue belongs to, if any 62 | public let milestone: Milestone? 63 | 64 | /// True if the issue has been closed by a contributor 65 | public let isLocked: Bool 66 | 67 | /// The number of comments 68 | public let commentCount: Int 69 | 70 | /// Contains the informations like the diff URL when the issue is a pull-request 71 | public let pullRequest: PullRequest? 72 | 73 | /// The date this issue was closed at, if it ever were 74 | public let closedAt: Date? 75 | 76 | /// The date this issue was created at 77 | public let createdAt: Date 78 | 79 | /// The date this issue was updated at 80 | public let updatedAt: Date 81 | 82 | public var description: String { 83 | return title 84 | } 85 | 86 | public init(id: ID, url: URL?, number: Int, state: State, title: String, body: String, user: UserInfo, labels: [Label], assignees: [UserInfo], milestone: Milestone?, isLocked: Bool, commentCount: Int, pullRequest: PullRequest?, closedAt: Date?, createdAt: Date, updatedAt: Date) { 87 | self.id = id 88 | self.url = url 89 | self.number = number 90 | self.state = state 91 | self.title = title 92 | self.body = body 93 | self.user = user 94 | self.milestone = milestone 95 | self.isLocked = isLocked 96 | self.commentCount = commentCount 97 | self.pullRequest = pullRequest 98 | self.labels = labels 99 | self.assignees = assignees 100 | self.closedAt = closedAt 101 | self.createdAt = createdAt 102 | self.updatedAt = updatedAt 103 | } 104 | 105 | private enum CodingKeys: String, CodingKey { 106 | case id 107 | case url = "html_url" 108 | case number 109 | case state 110 | case title 111 | case body 112 | case user 113 | case labels 114 | case assignees 115 | case milestone 116 | case isLocked = "locked" 117 | case commentCount = "comments" 118 | case pullRequest = "pull_request" 119 | case closedAt = "closed_at" 120 | case createdAt = "created_at" 121 | case updatedAt = "updated_at" 122 | } 123 | } 124 | 125 | extension Issue: Equatable { 126 | public static func ==(lhs: Issue, rhs: Issue) -> Bool { 127 | return lhs.id == rhs.id 128 | && lhs.url == rhs.url 129 | && lhs.number == rhs.number 130 | && lhs.state == rhs.state 131 | && lhs.title == rhs.title 132 | && lhs.body == rhs.body 133 | && lhs.isLocked == rhs.isLocked 134 | && lhs.commentCount == rhs.commentCount 135 | && lhs.createdAt == rhs.createdAt 136 | && lhs.updatedAt == rhs.updatedAt 137 | && lhs.labels == rhs.labels 138 | && lhs.milestone == rhs.milestone 139 | && lhs.pullRequest == rhs.pullRequest 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Sources/Tentacle/JSONExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONExtensions.swift 3 | // Tentacle 4 | // 5 | // Created by Matt Diephouse on 3/10/16. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | internal func decode(_ payload: Data) -> Result { 12 | Result { 13 | let decoder = JSONDecoder() 14 | decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601) 15 | return try decoder.decode(T.self, from: payload) 16 | }.mapError { $0 as! DecodingError } 17 | } 18 | 19 | internal func decodeList(_ payload: Data) -> Result<[T], DecodingError> { 20 | Result { () -> [T] in 21 | let decoder = JSONDecoder() 22 | decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601) 23 | return try decoder.decode([T].self, from: payload) 24 | }.mapError { $0 as! DecodingError } 25 | } 26 | 27 | extension DecodingError.Context: Equatable { 28 | public static func ==(lhs: DecodingError.Context, rhs: DecodingError.Context) -> Bool { 29 | return lhs.debugDescription == rhs.debugDescription 30 | } 31 | } 32 | 33 | extension DecodingError: Equatable { 34 | public static func ==(lhs: DecodingError, rhs: DecodingError) -> Bool { 35 | switch (lhs, rhs) { 36 | case (.dataCorrupted(let lContext), .dataCorrupted(let rContext)): 37 | return lContext == rContext 38 | case (.keyNotFound(let lKey, let lContext), .keyNotFound(let rKey, let rContext)): 39 | return lKey.stringValue == rKey.stringValue && lContext == rContext 40 | case (.typeMismatch(let lType, let lContext), .typeMismatch(let rType, let rContext)): 41 | return lType == rType && lContext == rContext 42 | case (.valueNotFound(let lType, let lContext), .valueNotFound(let rType, let rContext)): 43 | return lType == rType && lContext == rContext 44 | default: 45 | return false 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/Tentacle/Label.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Label.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2016-05-23. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Label: CustomStringConvertible, ResourceType { 12 | public let name: String 13 | public let color: Color 14 | 15 | public var description: String { 16 | return name 17 | } 18 | 19 | public init(from decoder: Decoder) throws { 20 | let container = try decoder.container(keyedBy: CodingKeys.self) 21 | self.name = try container.decode(String.self, forKey: .name) 22 | self.color = Color(hex: try container.decode(String.self, forKey: .color)) 23 | } 24 | 25 | public init(name: String, color: Color) { 26 | self.name = name 27 | self.color = color 28 | } 29 | 30 | private enum CodingKeys: String, CodingKey { 31 | case name 32 | case color 33 | } 34 | } 35 | 36 | extension Label: Hashable { 37 | public static func ==(lhs: Label, rhs: Label) -> Bool { 38 | return lhs.name == rhs.name 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /Sources/Tentacle/Milestone.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Milestone.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2016-05-23. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Milestone: CustomStringConvertible, ResourceType, Identifiable { 12 | public enum State: String, Decodable { 13 | case open 14 | case closed 15 | } 16 | 17 | /// The ID of the milestone 18 | public let id: ID 19 | 20 | /// The number of the milestone in the repository it belongs to 21 | public let number: Int 22 | 23 | /// The state of the Milestone, open or closed 24 | public let state: State 25 | 26 | /// The title of the milestone 27 | public let title: String 28 | 29 | /// The description of the milestone 30 | public let body: String 31 | 32 | /// The user who created the milestone 33 | public let creator: UserInfo 34 | 35 | /// The number of the open issues in the milestone 36 | public let openIssueCount: Int 37 | 38 | /// The number of closed issues in the milestone 39 | public let closedIssueCount: Int 40 | 41 | /// The date the milestone was created 42 | public let createdAt: Date 43 | 44 | /// The date the milestone was last updated at 45 | public let updatedAt: Date 46 | 47 | /// The date the milestone was closed at, if ever 48 | public let closedAt: Date? 49 | 50 | /// The date the milestone is due on 51 | public let dueOn: Date? 52 | 53 | /// The URL to view this milestone in a browser 54 | public let url: URL 55 | 56 | public var description: String { 57 | return title 58 | } 59 | 60 | private enum CodingKeys: String, CodingKey { 61 | case id 62 | case number 63 | case state 64 | case title 65 | case body = "description" 66 | case creator 67 | case openIssueCount = "open_issues" 68 | case closedIssueCount = "closed_issues" 69 | case createdAt = "created_at" 70 | case updatedAt = "updated_at" 71 | case closedAt = "closed_at" 72 | case dueOn = "due_on" 73 | case url = "html_url" 74 | } 75 | } 76 | 77 | extension Milestone: Equatable { 78 | public static func ==(lhs: Milestone, rhs: Milestone) -> Bool { 79 | return lhs.id == rhs.id 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/Tentacle/Organization.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Organization.swift 3 | // Tentacle 4 | // 5 | // Created by Matt Diephouse on 5/19/17. 6 | // Copyright © 2017 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Organization { 12 | /// A request for the organization's repositories. 13 | /// 14 | /// https://developer.github.com/v3/repos/#list-organization-repositories 15 | public var repositories: Request<[RepositoryInfo]> { 16 | return Request(method: .get, path: "/orgs/\(name)/repos") 17 | } 18 | } 19 | 20 | /// An organization on GitHub or GitHub Enterprise. 21 | public struct Organization: CustomStringConvertible, ResourceType { 22 | /// The organization's name. 23 | public let name: String 24 | 25 | public init(_ name: String) { 26 | self.name = name 27 | } 28 | 29 | public var description: String { 30 | return name 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Tentacle/PullRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PullRequest.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2016-05-23. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct PullRequest: CustomStringConvertible, ResourceType { 12 | /// The URL to view the Pull Request is an browser 13 | public let url: URL 14 | 15 | /// The URL to the diff showing all the changes included in this pull request 16 | public let diffURL: URL 17 | 18 | /// The URL to a downloadable patch for this pull request 19 | public let patchURL: URL 20 | 21 | public var description: String { 22 | return url.absoluteString 23 | } 24 | 25 | private enum CodingKeys: String, CodingKey { 26 | case url = "html_url" 27 | case diffURL = "diff_url" 28 | case patchURL = "patch_url" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Tentacle/Release.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Release.swift 3 | // Tentacle 4 | // 5 | // Created by Matt Diephouse on 3/3/16. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Repository { 12 | /// A request to get the latest release for the repository. 13 | /// 14 | /// If the repository doesn't have any releases, this will result in a `.doesNotExist` error. 15 | /// 16 | /// https://developer.github.com/v3/repos/releases/#get-the-latest-release 17 | public var latestRelease: Request { 18 | return Request(method: .get, path: "/repos/\(owner)/\(name)/releases/latest") 19 | } 20 | 21 | /// A request for the release corresponding to the given tag. 22 | /// 23 | /// If the tag exists, but there's not a correspoding GitHub Release, this will result in a 24 | /// `.doesNotExist` error. This is indistinguishable from a nonexistent tag. 25 | /// 26 | /// https://developer.github.com/v3/repos/releases/#get-a-release-by-tag-name 27 | public func release(forTag tag: String) -> Request { 28 | return Request(method: .get, path: "/repos/\(owner)/\(name)/releases/tags/\(tag)") 29 | } 30 | 31 | /// A request for the releases in the repository. 32 | /// 33 | /// https://developer.github.com/v3/repos/releases/#list-releases-for-a-repository 34 | public var releases: Request<[Release]> { 35 | return Request(method: .get, path: "/repos/\(owner)/\(name)/releases") 36 | } 37 | } 38 | 39 | /// A Release of a Repository. 40 | public struct Release: CustomStringConvertible, ResourceType, Identifiable { 41 | /// An Asset attached to a Release. 42 | public struct Asset: CustomStringConvertible, ResourceType, Identifiable { 43 | /// The unique ID for this release asset. 44 | public let id: ID 45 | 46 | /// The filename of this asset. 47 | public let name: String 48 | 49 | /// The MIME type of this asset. 50 | public let contentType: String 51 | 52 | /// The URL at which the asset can be downloaded directly. 53 | public let url: URL 54 | 55 | /// The URL at which the asset can be downloaded via the API. 56 | public let apiURL: URL 57 | 58 | public var description: String { 59 | return "\(url)" 60 | } 61 | 62 | public init(id: ID, name: String, contentType: String, url: URL, apiURL: URL) { 63 | self.id = id 64 | self.name = name 65 | self.contentType = contentType 66 | self.url = url 67 | self.apiURL = apiURL 68 | } 69 | 70 | private enum CodingKeys: String, CodingKey { 71 | case id 72 | case name 73 | case contentType = "content_type" 74 | case url = "browser_download_url" 75 | case apiURL = "url" 76 | } 77 | } 78 | 79 | /// The unique ID of the release. 80 | public let id: ID 81 | 82 | /// Whether this release is a draft (only visible to the authenticted user). 83 | public let isDraft: Bool 84 | 85 | /// Whether this release represents a prerelease version. 86 | public let isPrerelease: Bool 87 | 88 | /// The name of the tag upon which this release is based. 89 | public let tag: String 90 | 91 | /// The name of the release. 92 | public let name: String? 93 | 94 | /// The web URL of the release. 95 | public let url: URL 96 | 97 | /// Any assets attached to the release. 98 | public let assets: [Asset] 99 | 100 | public var description: String { 101 | return "\(url)" 102 | } 103 | 104 | public init(id: ID, tag: String, url: URL, name: String? = nil, isDraft: Bool = false, isPrerelease: Bool = false, assets: [Asset]) { 105 | self.id = id 106 | self.tag = tag 107 | self.url = url 108 | self.name = name 109 | self.isDraft = isDraft 110 | self.isPrerelease = isPrerelease 111 | self.assets = assets 112 | } 113 | 114 | private enum CodingKeys: String, CodingKey { 115 | case id 116 | case isDraft = "draft" 117 | case isPrerelease = "prerelease" 118 | case tag = "tag_name" 119 | case name 120 | case url = "html_url" 121 | case assets 122 | } 123 | } 124 | 125 | extension Release.Asset: Equatable { 126 | public static func ==(lhs: Release.Asset, rhs: Release.Asset) -> Bool { 127 | return lhs.id == rhs.id && lhs.url == rhs.url 128 | } 129 | } 130 | 131 | extension Release: Equatable { 132 | public static func ==(lhs: Release, rhs: Release) -> Bool { 133 | return lhs.id == rhs.id 134 | && lhs.tag == rhs.tag 135 | && lhs.url == rhs.url 136 | && lhs.name == rhs.name 137 | && lhs.isDraft == rhs.isDraft 138 | && lhs.isPrerelease == rhs.isPrerelease 139 | && lhs.assets == rhs.assets 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Sources/Tentacle/Repository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Repository.swift 3 | // Tentacle 4 | // 5 | // Created by Matt Diephouse on 3/3/16. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Repository { 12 | 13 | /// A request for the repository's Info 14 | /// 15 | /// https://developer.github.com/v3/repos/#get 16 | public var info: Request { 17 | return Request(method: .get, path: "/repos/\(owner)/\(name)") 18 | } 19 | 20 | } 21 | 22 | 23 | /// A GitHub.com or GitHub Enterprise repository. 24 | public struct Repository: CustomStringConvertible { 25 | public let owner: String 26 | public let name: String 27 | 28 | public init(owner: String, name: String) { 29 | self.owner = owner 30 | self.name = name 31 | } 32 | 33 | public var description: String { 34 | return "\(owner)/\(name)" 35 | } 36 | } 37 | 38 | extension Repository: Hashable { 39 | public static func ==(lhs: Repository, rhs: Repository) -> Bool { 40 | return lhs.owner.caseInsensitiveCompare(rhs.owner) == .orderedSame 41 | && lhs.name.caseInsensitiveCompare(rhs.name) == .orderedSame 42 | } 43 | 44 | public func hash(into hasher: inout Hasher) { 45 | description.lowercased().hash(into: &hasher) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/Tentacle/RepositoryInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepositoryInfo.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2016-08-02. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct RepositoryInfo: CustomStringConvertible, ResourceType, Identifiable { 12 | /// The id of the repository 13 | public let id: ID 14 | 15 | /// The basic informations about the owner of the repository, either an User or an Organization 16 | public let owner: UserInfo 17 | 18 | /// The name of the repository 19 | public let name: String 20 | 21 | /// The name of the repository prefixed with the name of the owner 22 | public let nameWithOwner: String 23 | 24 | /// The description of the repository 25 | public let body: String? 26 | 27 | /// The URL of the repository to load in a browser 28 | public let url: URL 29 | 30 | /// The URL of the homepage for this repository 31 | public let homepage: URL? 32 | 33 | /// Contains true if the repository is private 34 | public let isPrivate: Bool 35 | 36 | /// Contains true if the repository is a fork 37 | public let isFork: Bool 38 | 39 | /// The number of forks of this repository 40 | public let forksCount: Int 41 | 42 | /// The number of users who starred this repository 43 | public let stargazersCount: Int 44 | 45 | /// The number of users watching this repository 46 | public let watchersCount: Int 47 | 48 | /// The number of open issues in this repository 49 | public let openIssuesCount: Int 50 | 51 | /// The date the last push happened at 52 | public let pushedAt: Date 53 | 54 | /// The date the repository was created at 55 | public let createdAt: Date 56 | 57 | /// The date the repository was last updated 58 | public let updatedAt: Date 59 | 60 | public var description: String { 61 | return nameWithOwner 62 | } 63 | 64 | public init(from decoder: Decoder) throws { 65 | let container = try decoder.container(keyedBy: CodingKeys.self) 66 | 67 | self.id = try container.decode(ID.self, forKey: .id) 68 | self.owner = try container.decode(UserInfo.self, forKey: .owner) 69 | self.name = try container.decode(String.self, forKey: .name) 70 | self.nameWithOwner = try container.decode(String.self, forKey: .nameWithOwner) 71 | self.body = try container.decodeIfPresent(String.self, forKey: .body) 72 | self.url = try container.decode(URL.self, forKey: .url) 73 | self.homepage = try? container.decode(URL.self, forKey: .homepage) 74 | self.isPrivate = try container.decode(Bool.self, forKey: .isPrivate) 75 | self.isFork = try container.decode(Bool.self, forKey: .isFork) 76 | self.forksCount = try container.decode(Int.self, forKey: .forksCount) 77 | self.stargazersCount = try container.decode(Int.self, forKey: .stargazersCount) 78 | self.watchersCount = try container.decode(Int.self, forKey: .watchersCount) 79 | self.openIssuesCount = try container.decode(Int.self, forKey: .openIssuesCount) 80 | self.pushedAt = try container.decode(Date.self, forKey: .pushedAt) 81 | self.createdAt = try container.decode(Date.self, forKey: .createdAt) 82 | self.updatedAt = try container.decode(Date.self, forKey: .updatedAt) 83 | } 84 | 85 | public init(id: ID, owner: UserInfo, name: String, nameWithOwner: String, body: String?, url: URL, homepage: URL?, isPrivate: Bool, isFork: Bool, forksCount: Int, stargazersCount: Int, watchersCount: Int, openIssuesCount: Int, pushedAt: Date, createdAt: Date, updatedAt: Date) { 86 | self.id = id 87 | self.owner = owner 88 | self.name = name 89 | self.nameWithOwner = nameWithOwner 90 | self.body = body 91 | self.url = url 92 | self.homepage = homepage 93 | self.isPrivate = isPrivate 94 | self.isFork = isFork 95 | self.forksCount = forksCount 96 | self.stargazersCount = stargazersCount 97 | self.watchersCount = watchersCount 98 | self.openIssuesCount = openIssuesCount 99 | self.pushedAt = pushedAt 100 | self.createdAt = createdAt 101 | self.updatedAt = updatedAt 102 | } 103 | 104 | private enum CodingKeys: String, CodingKey { 105 | case id 106 | case owner 107 | case name 108 | case nameWithOwner = "full_name" 109 | case body = "description" 110 | case url = "html_url" 111 | case homepage 112 | case isPrivate = "private" 113 | case isFork = "fork" 114 | case forksCount = "forks_count" 115 | case stargazersCount = "stargazers_count" 116 | case watchersCount = "watchers_count" 117 | case openIssuesCount = "open_issues_count" 118 | case pushedAt = "pushed_at" 119 | case createdAt = "created_at" 120 | case updatedAt = "updated_at" 121 | } 122 | } 123 | 124 | extension RepositoryInfo: Hashable { 125 | public static func ==(lhs: RepositoryInfo, rhs: RepositoryInfo) -> Bool { 126 | return lhs.id == rhs.id 127 | && lhs.name == rhs.name 128 | && lhs.nameWithOwner == rhs.nameWithOwner 129 | } 130 | 131 | public func hash(into hasher: inout Hasher) { 132 | id.hash(into: &hasher) 133 | nameWithOwner.hash(into: &hasher) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Sources/Tentacle/Request.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Request.swift 3 | // Tentacle 4 | // 5 | // Created by Matt Diephouse on 5/19/17. 6 | // Copyright © 2017 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | internal enum Method: String { 12 | case get = "GET" 13 | case post = "POST" 14 | case put = "PUT" 15 | case head = "HEAD" 16 | case options = "OPTIONS" 17 | } 18 | 19 | /// An opaque value representing a request to be made. 20 | public struct Request: Hashable { 21 | internal var method: Method 22 | internal var path: String 23 | internal var queryItems: [URLQueryItem] 24 | internal var body: Data? 25 | 26 | internal init(method: Method = .get, path: String, queryItems: [URLQueryItem] = [], body: Data? = nil) { 27 | self.method = method 28 | self.path = path 29 | self.queryItems = queryItems 30 | self.body = body 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Tentacle/ResourceType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResourceType.swift 3 | // Tentacle 4 | // 5 | // Created by Matt Diephouse on 3/10/16. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A Resource from the GitHub API. 12 | public protocol ResourceType: Decodable, Hashable { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Tentacle/Response.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Response.swift 3 | // Tentacle 4 | // 5 | // Created by Matt Diephouse on 3/10/16. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | let LinksRegex = try! NSRegularExpression(pattern: "(?<=\\A|,) *<([^>]+)>( *; *\\w+ *= *\"[^\"]+\")* *(?=\\z|,)") 12 | let LinkParamRegex = try! NSRegularExpression(pattern: "; *(\\w+) *= *\"([^\"]+)\"") 13 | 14 | /// Returns any links, keyed by `rel`, from the RFC 5988 link header. 15 | private func linksInLinkHeader(_ header: String) -> [String: URL] { 16 | var links: [String: URL] = [:] 17 | for match in LinksRegex.matches(in: header, range: NSRange(header.startIndex..., in: header)) { 18 | let URI = String(header[Range(match.range(at: 1), in: header)!]) 19 | let params = String(header[Range(match.range(at: 2), in: header)!]) 20 | guard let url = URL(string: URI) else { continue } 21 | 22 | var relName: String? = nil 23 | for match in LinkParamRegex.matches(in: params, range: NSRange(params.startIndex..., in: params)) { 24 | let name = params[Range(match.range(at: 1), in: params)!] 25 | if name != "rel" { continue } 26 | 27 | relName = String(params[Range(match.range(at: 2), in: params)!]) 28 | } 29 | 30 | if let relName = relName { 31 | links[relName] = url 32 | } 33 | } 34 | return links 35 | } 36 | 37 | /// A response from the GitHub API. 38 | public struct Response: Hashable { 39 | /// The number of requests remaining in the current rate limit window, or nil if the server 40 | /// isn't rate-limited. 41 | public let rateLimitRemaining: UInt? 42 | 43 | /// The time at which the current rate limit window resets, or nil if the server isn't 44 | /// rate-limited. 45 | public let rateLimitReset: Date? 46 | 47 | /// Any links that are included in the response. 48 | public let links: [String: URL] 49 | 50 | public init(rateLimitRemaining: UInt, rateLimitReset: Date, links: [String: URL]) { 51 | self.rateLimitRemaining = rateLimitRemaining 52 | self.rateLimitReset = rateLimitReset 53 | self.links = links 54 | } 55 | 56 | /// Initialize a response with HTTP header fields. 57 | internal init(headerFields: [String : String]) { 58 | self.rateLimitRemaining = headerFields["X-RateLimit-Remaining"].flatMap { UInt($0) } 59 | self.rateLimitReset = headerFields["X-RateLimit-Reset"] 60 | .flatMap { TimeInterval($0) } 61 | .map { Date(timeIntervalSince1970: $0) } 62 | self.links = linksInLinkHeader(headerFields["Link"] as String? ?? "") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Tentacle/Server.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Server.swift 3 | // Tentacle 4 | // 5 | // Created by Matt Diephouse on 3/3/16. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | /// A GitHub.com or GitHub Enterprise server. 13 | public enum Server: CustomStringConvertible { 14 | /// The GitHub.com server. 15 | case dotCom 16 | 17 | /// A GitHub Enterprise server. 18 | case enterprise(url: URL) 19 | 20 | /// The URL of the server. 21 | public var url: URL { 22 | switch self { 23 | case .dotCom: 24 | return URL(string: "https://github.com")! 25 | 26 | case let .enterprise(url): 27 | return url 28 | } 29 | } 30 | 31 | internal var endpoint: String { 32 | switch self { 33 | case .dotCom: 34 | return "https://api.github.com" 35 | 36 | case let .enterprise(url): 37 | return "\(url.scheme!)://\(url.host!)/api/v3" 38 | } 39 | } 40 | 41 | public var description: String { 42 | return "\(url)" 43 | } 44 | 45 | /// The URL of the given repository. 46 | public func url(for repository: Repository) -> URL { 47 | return url 48 | .appendingPathComponent(repository.owner) 49 | .appendingPathComponent(repository.name) 50 | } 51 | } 52 | 53 | extension Server: Hashable { 54 | public static func ==(lhs: Server, rhs: Server) -> Bool { 55 | switch (lhs, rhs) { 56 | case (.dotCom, .dotCom): 57 | return true 58 | 59 | case (.enterprise, .enterprise): 60 | return lhs.endpoint.caseInsensitiveCompare(rhs.endpoint) == .orderedSame 61 | 62 | default: 63 | return false 64 | } 65 | } 66 | 67 | public func hash(into hasher: inout Hasher) { 68 | endpoint.lowercased().hash(into: &hasher) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/Tentacle/Sha.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sha.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2016-12-26. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct SHA: ResourceType, Encodable { 12 | public let hash: String 13 | 14 | public init(from decoder: Decoder) throws { 15 | let container = try decoder.singleValueContainer() 16 | self.hash = try container.decode(String.self) 17 | } 18 | 19 | public func encode(to encoder: Encoder) throws { 20 | var container = encoder.singleValueContainer() 21 | try container.encode(hash) 22 | } 23 | } 24 | 25 | extension SHA: ExpressibleByStringLiteral { 26 | public init(stringLiteral value: String) { 27 | self.hash = value 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Tentacle/Tentacle.h: -------------------------------------------------------------------------------- 1 | // 2 | // Tentacle.h 3 | // Tentacle 4 | // 5 | // Created by Matt Diephouse on 3/3/16. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Tentacle. 12 | FOUNDATION_EXPORT double TentacleVersionNumber; 13 | 14 | //! Project version string for Tentacle. 15 | FOUNDATION_EXPORT const unsigned char TentacleVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/Tentacle/Tree.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tree.swift 3 | // Tentacle 4 | // 5 | // Created by David Caunt on 21/04/2017. 6 | // Copyright © 2017 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Repository { 12 | /// A request for a tree in the repository. 13 | /// 14 | /// https://developer.github.com/v3/git/trees/#get-a-tree 15 | public func tree(atRef ref: String = "HEAD", recursive: Bool = false) -> Request { 16 | let queryItems: [URLQueryItem] 17 | if recursive { 18 | queryItems = [ URLQueryItem(name: "recursive", value: "1") ] 19 | } else { 20 | queryItems = [] 21 | } 22 | return Request(method: .get, path: "repos/\(owner)/\(name)/git/trees/\(ref)", queryItems: queryItems) 23 | } 24 | 25 | /// A request to create a tree in the repository. 26 | /// 27 | /// https://developer.github.com/v3/git/trees/#create-a-tree 28 | public func create(tree: [Tree.Entry], basedOn base: String?) -> Request { 29 | let object = NewTree(entries: tree, base: base) 30 | 31 | let encoder = JSONEncoder() 32 | let payload = try? encoder.encode(object) 33 | return Request(method: .post, path: "repos/\(owner)/\(name)/git/trees", body: payload) 34 | } 35 | } 36 | 37 | public struct Tree: CustomStringConvertible, ResourceType { 38 | 39 | /// The SHA of the entry. 40 | public let sha: SHA 41 | 42 | /// The URL for the tree. 43 | public let url: URL 44 | 45 | /// The entries under this tree. 46 | public let entries: [Entry] 47 | 48 | /// Whether the number of entries in this tree exceeded the maximum number 49 | /// which will be returned by the API. 50 | public let isTruncated: Bool 51 | 52 | public var description: String { 53 | return "\(url)" 54 | } 55 | 56 | private enum CodingKeys: String, CodingKey { 57 | case sha 58 | case url 59 | case entries = "tree" 60 | case isTruncated = "truncated" 61 | } 62 | 63 | public struct Entry: ResourceType, Encodable { 64 | 65 | public enum EntryType: ResourceType, Encodable { 66 | case blob(url: URL, size: Int) 67 | case tree(url: URL) 68 | case commit 69 | 70 | public init(from decoder: Decoder) throws { 71 | let container = try decoder.container(keyedBy: CodingKeys.self) 72 | let type = try container.decode(String.self, forKey: .type) 73 | 74 | switch type { 75 | case "blob": 76 | let url = try container.decode(URL.self, forKey: .url) 77 | let size = try container.decode(Int.self, forKey: .size) 78 | self = .blob(url: url, size: size) 79 | case "commit": 80 | self = .commit 81 | case "tree": 82 | let url = try container.decode(URL.self, forKey: .url) 83 | self = .tree(url: url) 84 | default: 85 | throw DecodingError.dataCorruptedError( 86 | forKey: CodingKeys.type, 87 | in: container, 88 | debugDescription: "Unexpected type \(type)" 89 | ) 90 | } 91 | } 92 | 93 | private enum CodingKeys: String, CodingKey { 94 | case type 95 | case url 96 | case size 97 | } 98 | 99 | public func encode(to encoder: Encoder) throws { 100 | var container = encoder.singleValueContainer() 101 | switch self { 102 | case .blob: try container.encode("blob") 103 | case .tree: try container.encode("tree") 104 | case .commit: try container.encode("commit") 105 | } 106 | } 107 | } 108 | 109 | public enum Mode: String, ResourceType, Encodable { 110 | case file = "100644" 111 | case executable = "100755" 112 | case subdirectory = "040000" 113 | case submodule = "160000" 114 | case symlink = "120000" 115 | 116 | public func encode(to encoder: Encoder) throws { 117 | var container = encoder.singleValueContainer() 118 | try container.encode(rawValue) 119 | } 120 | 121 | } 122 | 123 | /// The type of the entry. 124 | public let type: EntryType 125 | 126 | /// The SHA of the entry. 127 | public let sha: SHA 128 | 129 | /// The repository-relative path of the entry. 130 | public let path: String 131 | 132 | /// The mode of the entry. 133 | public let mode: Mode 134 | 135 | public init(from decoder: Decoder) throws { 136 | let container = try decoder.container(keyedBy: CodingKeys.self) 137 | self.sha = try container.decode(SHA.self, forKey: .sha) 138 | self.path = try container.decode(String.self, forKey: .path) 139 | self.mode = try container.decode(Mode.self, forKey: .mode) 140 | self.type = try EntryType(from: decoder) 141 | } 142 | 143 | public init(type: EntryType, sha: SHA, path: String, mode: Mode) { 144 | self.type = type 145 | self.sha = sha 146 | self.path = path 147 | self.mode = mode 148 | } 149 | 150 | private enum CodingKeys: String, CodingKey { 151 | case type 152 | case sha 153 | case path 154 | case mode 155 | case url 156 | case size 157 | } 158 | 159 | public func encode(to encoder: Encoder) throws { 160 | var container = encoder.container(keyedBy: CodingKeys.self) 161 | try container.encode(type, forKey: .type) 162 | try container.encode(path, forKey: .path) 163 | try container.encode(sha, forKey: .sha) 164 | try container.encode(mode, forKey: .mode) 165 | 166 | switch type { 167 | case let .blob(url: url, size: size): 168 | try container.encode(url, forKey: .url) 169 | try container.encode(size, forKey: .size) 170 | case let .tree(url: url): 171 | try container.encode(url, forKey: .url) 172 | default: break 173 | } 174 | } 175 | } 176 | 177 | public func hash(into hasher: inout Hasher) { 178 | sha.hash(into: &hasher) 179 | } 180 | } 181 | 182 | extension Tree.Entry: Hashable { 183 | public static func ==(lhs: Tree.Entry, rhs: Tree.Entry) -> Bool { 184 | return lhs.sha == rhs.sha 185 | } 186 | 187 | public func hash(into hasher: inout Hasher) { 188 | sha.hash(into: &hasher) 189 | } 190 | } 191 | 192 | 193 | internal struct NewTree: Encodable { 194 | /// The entries under this tree. 195 | internal let entries: [Tree.Entry] 196 | 197 | /// The base for the new tree. 198 | internal let base: String? 199 | 200 | internal enum CodingKeys: String, CodingKey { 201 | case entries = "tree" 202 | case base = "base_tree" 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /Sources/Tentacle/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // Tentacle 4 | // 5 | // Created by Matt Diephouse on 4/12/16. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension User { 12 | /// A request for issues assigned to the authenticated user. 13 | /// 14 | /// https://developer.github.com/v3/issues/#list-issues 15 | static public var assignedIssues: Request<[Issue]> { 16 | return Request(method: .get, path: "/issues") 17 | } 18 | 19 | /// A request for the authenticated user's profile. 20 | /// 21 | /// https://developer.github.com/v3/users/#get-the-authenticated-user 22 | static public var profile: Request { 23 | return Request(method: .get, path: "/user") 24 | } 25 | 26 | /// A request for the authenticated user's public repositories. 27 | /// 28 | /// https://developer.github.com/v3/repos/#list-all-public-repositories 29 | static public var publicRepositories: Request<[RepositoryInfo]> { 30 | return Request(method: .get, path: "/repositories") 31 | } 32 | 33 | /// A request for the authenticated user's repositories. 34 | /// 35 | /// https://developer.github.com/v3/repos/#list-your-repositories 36 | static public var repositories: Request<[RepositoryInfo]> { 37 | return Request(method: .get, path: "/user/repos") 38 | } 39 | } 40 | 41 | extension User { 42 | /// A request for the user's profile. 43 | /// 44 | /// https://developer.github.com/v3/users/#get-a-single-user 45 | public var profile: Request { 46 | return Request(method: .get, path: "/users/\(login)") 47 | } 48 | 49 | /// A request for the user's repositories. 50 | /// 51 | /// https://developer.github.com/v3/repos/#list-user-repositories 52 | public var repositories: Request<[RepositoryInfo]> { 53 | return Request(method: .get, path: "/users/\(login)/repos") 54 | } 55 | } 56 | 57 | /// A user on GitHub or GitHub Enterprise. 58 | public struct User: CustomStringConvertible, ResourceType { 59 | /// The user's login/username. 60 | public let login: String 61 | 62 | public init(_ login: String) { 63 | self.login = login 64 | } 65 | 66 | public var description: String { 67 | return login 68 | } 69 | } 70 | 71 | /// Information about a user on GitHub. 72 | public struct UserInfo: CustomStringConvertible, ResourceType, Identifiable { 73 | public enum UserType: String, Decodable { 74 | case user = "User" 75 | case organization = "Organization" 76 | } 77 | 78 | /// The unique ID of the user. 79 | public let id: ID 80 | 81 | /// The user this information is about. 82 | public let user: User 83 | 84 | /// The URL of the user's GitHub page. 85 | public let url: URL 86 | 87 | /// The URL of the user's avatar. 88 | public let avatarURL: URL 89 | 90 | /// The type of user if it's a regular one or an organization 91 | public let type: UserType 92 | 93 | public var description: String { 94 | return user.description 95 | } 96 | 97 | private enum CodingKeys: String, CodingKey { 98 | case id 99 | case user = "login" 100 | case url = "html_url" 101 | case avatarURL = "avatar_url" 102 | case type 103 | } 104 | 105 | public init(id: ID, user: User, url: URL, avatarURL: URL, type: UserType) { 106 | self.id = id 107 | self.user = user 108 | self.url = url 109 | self.avatarURL = avatarURL 110 | self.type = type 111 | } 112 | 113 | public init(from decoder: Decoder) throws { 114 | let container = try decoder.container(keyedBy: CodingKeys.self) 115 | self.id = try container.decode(ID.self, forKey: .id) 116 | self.user = try User(from: decoder) 117 | self.url = try container.decode(URL.self, forKey: .url) 118 | self.avatarURL = try container.decode(URL.self, forKey: .avatarURL) 119 | self.type = try container.decode(UserType.self, forKey: .type) 120 | } 121 | } 122 | 123 | /// Extended information about a user on GitHub. 124 | public struct UserProfile: ResourceType { 125 | /// The user that this information refers to. 126 | public let user: UserInfo 127 | 128 | /// The date that the user joined GitHub. 129 | public let joinedDate: Date 130 | 131 | /// The user's name if they've set one. 132 | public let name: String? 133 | 134 | /// The user's public email address if they've set one. 135 | public let email: String? 136 | 137 | /// The URL of the user's website if they've set one 138 | /// (the type here is a String because Github lets you use 139 | /// anything and doesn't validate that you've entered a valid URL) 140 | public let websiteURL: String? 141 | 142 | /// The user's company if they've set one. 143 | public let company: String? 144 | 145 | public var description: String { 146 | return user.description 147 | } 148 | 149 | public init(user: UserInfo, joinedDate: Date, name: String?, email: String?, websiteURL: String?, company: String?) { 150 | self.user = user 151 | self.joinedDate = joinedDate 152 | self.name = name 153 | self.email = email 154 | self.websiteURL = websiteURL 155 | self.company = company 156 | } 157 | 158 | public init(from decoder: Decoder) throws { 159 | self.user = try UserInfo(from: decoder) 160 | 161 | let container = try decoder.container(keyedBy: CodingKeys.self) 162 | self.joinedDate = try container.decode(Date.self, forKey: .joinedDate) 163 | self.name = try container.decodeIfPresent(String.self, forKey: .name) 164 | self.email = try container.decodeIfPresent(String.self, forKey: .email) 165 | self.websiteURL = try container.decodeIfPresent(String.self, forKey: .websiteURL) 166 | self.company = try container.decodeIfPresent(String.self, forKey: .company) 167 | } 168 | 169 | private enum CodingKeys: String, CodingKey { 170 | case user 171 | case joinedDate = "created_at" 172 | case name 173 | case email 174 | case websiteURL = "blog" 175 | case company 176 | } 177 | 178 | public func hash(into hasher: inout Hasher) { 179 | user.hash(into: &hasher) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /Tentacle.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tentacle.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tentacle.xcodeproj/xcshareddata/xcschemes/Tentacle-OSX.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 76 | 77 | 83 | 84 | 85 | 86 | 92 | 93 | 99 | 100 | 101 | 102 | 104 | 105 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /Tentacle.xcodeproj/xcshareddata/xcschemes/Tentacle-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 65 | 66 | 67 | 68 | 70 | 76 | 77 | 78 | 79 | 80 | 90 | 91 | 97 | 98 | 99 | 100 | 106 | 107 | 113 | 114 | 115 | 116 | 118 | 119 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /Tentacle.xcodeproj/xcshareddata/xcschemes/update-test-fixtures.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /Tentacle.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Tentacle.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tentacle.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tests/TentacleTests/BranchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BranchTests.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2017-02-15. 6 | // Copyright © 2017 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tentacle 11 | 12 | class BranchTests: XCTestCase { 13 | 14 | func testDecodingBranches() { 15 | let expected = [ 16 | Branch(name: "debuggin", commit: Branch.Commit(sha: "117775803ff583c467dac3cd2c923b8d3f7d1869")), 17 | Branch(name: "master", commit: Branch.Commit(sha: "e1396a56055812234e97aeda78731d7228e0bbc7")), 18 | Branch(name: "playground", commit: Branch.Commit(sha: "131709d54e1157699e44300cb9b9f8d22f2807e7")), 19 | Branch(name: "release-0.17.0", commit: Branch.Commit(sha: "0f6162a44d56b3e2fc3e42d20e77b8b09bd5e00a")) 20 | ] 21 | 22 | let fixture: [Branch] = Fixture.BranchesForRepository.BranchesInReactiveTask.decodeList()! 23 | XCTAssertEqual(fixture, expected) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/TentacleTests/ClientTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClientTests.swift 3 | // Tentacle 4 | // 5 | // Created by Matt Diephouse on 3/5/16. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import ReactiveSwift 10 | import Tentacle 11 | import XCTest 12 | 13 | func ExpectResult 14 | 15 | (_ producer: SignalProducer<(Response, O), Client.Error>, _ result: Result<[O], Client.Error>, file: StaticString = #file, line: UInt = #line) 16 | { 17 | let actual = producer.map { $0.1 }.collect().single()! 18 | let message: String 19 | switch result { 20 | case let .success(value): 21 | message = "\(actual) is not equal to \(value)" 22 | case let .failure(error): 23 | message = "\(actual) is not equal to \(error)" 24 | } 25 | XCTAssertTrue(actual == result, message, file: file, line: line) 26 | } 27 | 28 | func ExpectResult 29 | 30 | (_ producer: SignalProducer<(Response, O), Client.Error>, _ result: Result<[F], Client.Error>, file: StaticString = #file, line: UInt = #line) 31 | { 32 | let expected = result.map { fixtures -> [O] in fixtures.map { $0.decode()! } } 33 | ExpectResult(producer, expected, file: file, line: line) 34 | } 35 | 36 | func ExpectResult 37 | 38 | (_ producer: SignalProducer<(Response, [O]), Client.Error>, _ result: Result<[[O]], Client.Error>, file: StaticString = #file, line: UInt = #line) 39 | { 40 | let actual = producer.map { $0.1 }.collect().single()! 41 | let message: String 42 | switch result { 43 | case let .success(value): 44 | message = "\(actual) is not equal to \(value)" 45 | case let .failure(error): 46 | message = "\(actual) is not equal to \(error)" 47 | } 48 | XCTAssertTrue(actual == result, message, file: file, line: line) 49 | } 50 | 51 | func ExpectResult 52 | 53 | (_ producer: SignalProducer<(Response, [O]), Client.Error>, _ result: Result, file: StaticString = #file, line: UInt = #line) where C.Iterator.Element == F 54 | { 55 | let expected = result.map { fixtures -> [[O]] in fixtures.map { $0.decode()! } } 56 | ExpectResult(producer, expected, file: file, line: line) 57 | } 58 | 59 | func ExpectError 60 | 61 | (_ producer: SignalProducer<(Response, O), Client.Error>, _ error: Client.Error, file: StaticString = #file, line: UInt = #line) 62 | { 63 | ExpectResult(producer, Result<[O], Client.Error>.failure(error), file: file, line: line) 64 | } 65 | 66 | func ExpectFixtures 67 | 68 | (_ producer: SignalProducer<(Response, O), Client.Error>, _ fixtures: F..., file: StaticString = #file, line: UInt = #line) 69 | { 70 | ExpectResult(producer, Result<[F], Client.Error>.success(fixtures), file: file, line: line) 71 | } 72 | 73 | func ExpectFixtures 74 | 75 | (_ producer: SignalProducer<(Response, [O]), Client.Error>, _ fixtures: C, file: StaticString = #file, line: UInt = #line) where C.Iterator.Element == F 76 | { 77 | ExpectResult(producer, .success(fixtures), file: file, line: line) 78 | } 79 | 80 | class ClientTests: XCTestCase { 81 | private let client = Client(.dotCom) 82 | 83 | override func setUp() { 84 | HTTPStub.shared.stubRequests = { request in 85 | guard let fixture = Fixture.fixtureForURL(request.url!) else { 86 | fatalError("No Fixture found for url \(request.url!)") 87 | } 88 | 89 | return fixture 90 | } 91 | } 92 | 93 | func testReleasesInRepository() { 94 | let fixtures = Fixture.Releases.Carthage 95 | 96 | ExpectFixtures( 97 | client.execute(fixtures[0].request), 98 | fixtures 99 | ) 100 | } 101 | 102 | func testReleasesInRepositoryPage2() { 103 | let fixtures = Fixture.Releases.Carthage 104 | ExpectFixtures( 105 | client.execute(fixtures[0].request, page: 2), 106 | fixtures.dropFirst() 107 | ) 108 | } 109 | 110 | func testReleaseForTagInRepository() { 111 | let fixture = Fixture.Release.Carthage0_15 112 | ExpectFixtures( 113 | client.execute(fixture.request), 114 | fixture 115 | ) 116 | } 117 | 118 | func testReleaseForTagInRepositoryNonExistent() { 119 | let fixture = Fixture.Release.Nonexistent 120 | ExpectError( 121 | client.execute(fixture.request), 122 | .doesNotExist 123 | ) 124 | } 125 | 126 | func testReleaseForTagInRepositoryTagOnly() { 127 | let fixture = Fixture.Release.TagOnly 128 | ExpectError( 129 | client.execute(fixture.request), 130 | .doesNotExist 131 | ) 132 | } 133 | 134 | func testDownloadAsset() throws { 135 | let release: Release = Fixture.Release.MDPSplitView1_0_2.decode()! 136 | let asset = release.assets 137 | .first { $0.name == "MDPSplitView.framework.zip" }! 138 | 139 | let result = client 140 | .download(asset: asset) 141 | .map { url in 142 | return try! Data(contentsOf: url) 143 | } 144 | .single()! 145 | 146 | XCTAssertEqual(try result.get(), Fixture.Release.Asset.MDPSplitView_framework_zip.data) 147 | } 148 | 149 | func testUserWithLogin() { 150 | let fixture = Fixture.UserProfile.mdiep 151 | ExpectFixtures(client.execute(fixture.request), fixture) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Tests/TentacleTests/ColorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorTests.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2016-07-19. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tentacle 11 | 12 | class ColorTests: XCTestCase { 13 | 14 | func testColorsAreProperlyDecoded() { 15 | XCTAssertEqual(Color(hex: "ffffff"), Color(red: 1, green: 1, blue: 1, alpha: 1)) 16 | XCTAssertEqual(Color(hex: "ff0000"), Color(red: 1, green: 0, blue: 0, alpha: 1)) 17 | XCTAssertEqual(Color(hex: "00ff00"), Color(red: 0, green: 1, blue: 0, alpha: 1)) 18 | XCTAssertEqual(Color(hex: "0000ff"), Color(red: 0, green: 0, blue: 1, alpha: 1)) 19 | XCTAssertEqual(Color(hex: "000000"), Color(red: 0, green: 0, blue: 0, alpha: 1)) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Tests/TentacleTests/CommentsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommentsTests.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2016-07-27. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | @testable import Tentacle 10 | import XCTest 11 | 12 | class CommentsTests: XCTestCase { 13 | 14 | func testDecodedCommentsOnSampleRepositoryIssue() { 15 | let palleasOpensource = UserInfo( 16 | id: 15802020, 17 | user: User("Palleas-opensource"), 18 | url: URL(string: "https://github.com/Palleas-opensource")!, 19 | avatarURL: URL(string: "https://avatars.githubusercontent.com/u/15802020?v=3")!, 20 | type: .user 21 | ) 22 | 23 | let palleas = UserInfo( 24 | id: 48797, 25 | user: User("Palleas"), 26 | url: URL(string: "https://github.com/Palleas")!, 27 | avatarURL: URL(string: "https://avatars.githubusercontent.com/u/15802020?v=3")!, 28 | type: .user 29 | ) 30 | 31 | let expected: [Comment] = [ 32 | Comment( 33 | id: 235455442, 34 | url: URL(string: "https://github.com/Palleas-opensource/Sample-repository/issues/1#issuecomment-235455442")!, 35 | createdAt: DateFormatter.iso8601.date(from: "2016-07-27T01:28:21Z")!, 36 | updatedAt: DateFormatter.iso8601.date(from: "2016-07-27T01:28:21Z")!, 37 | body: "I know right?!\n", 38 | author: palleas 39 | ), 40 | Comment( 41 | id: 235455603, 42 | url: URL(string: "https://github.com/Palleas-opensource/Sample-repository/issues/1#issuecomment-235455603")!, 43 | createdAt: DateFormatter.iso8601.date(from: "2016-07-27T01:29:31Z")!, 44 | updatedAt: DateFormatter.iso8601.date(from: "2016-07-27T01:29:31Z")!, 45 | body: "👍 Good idea to say stuff like that on internet!\n", 46 | author: palleasOpensource 47 | ) 48 | ] 49 | 50 | let comments: [Comment]? = Fixture.CommentsOnIssue.CommentsOnIssueInSampleRepository.decode() 51 | 52 | XCTAssertEqual(comments!, expected) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Tests/TentacleTests/ContentTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentTests.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2016-11-28. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tentacle 11 | 12 | class ContentTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | 17 | HTTPStub.shared.initialize() 18 | } 19 | 20 | func testDecodedFile() { 21 | let expected: Content = .file(Content.File( 22 | content: .file(size: 19, downloadURL: URL(string: "https://raw.githubusercontent.com/Palleas-opensource/Sample-repository/master/README.md")!), 23 | name: "README.md", 24 | path: "README.md", 25 | sha: "28ec72028c4ae47de689964a23ebb223f10cfe80", 26 | url: URL(string: "https://github.com/Palleas-opensource/Sample-repository/blob/master/README.md")! 27 | )) 28 | 29 | XCTAssertEqual(Fixture.FileForRepository.ReadMeForSampleRepository.decode()!, expected) 30 | } 31 | 32 | func testDecodedDirectory() { 33 | let expected: Content = .directory([ 34 | Content.File( 35 | content: .directory, 36 | name: "Directory", 37 | path: "Tools/Directory", 38 | sha: "5bfad2b3f8e483b6b173d8aaff19597e84626f15", 39 | url: URL(string: "https://github.com/Palleas-opensource/Sample-repository/tree/master/Tools/Directory")! 40 | ), 41 | Content.File( 42 | content: .file(size: 18, downloadURL: URL(string: "https://raw.githubusercontent.com/Palleas-opensource/Sample-repository/master/Tools/README.markdown")!), 43 | name: "README.markdown", 44 | path: "Tools/README.markdown", 45 | sha: "c3eb8708a0a5aaa4f685aab24ef6403fbfd28efc", 46 | url: URL(string: "https://github.com/Palleas-opensource/Sample-repository/blob/master/Tools/README.markdown")! 47 | ), 48 | Content.File( 49 | content: .submodule(url: nil), 50 | name: "Tentacle", 51 | path: "Tools/Tentacle", 52 | sha: "7a84505a3c553fd8e2879cfa63753b0cd212feb8", 53 | url: URL(string: "https://github.com/mdiep/Tentacle/tree/7a84505a3c553fd8e2879cfa63753b0cd212feb8")! 54 | ), 55 | Content.File( 56 | content: .symlink(target: nil, downloadURL: URL(string: "https://raw.githubusercontent.com/Palleas-opensource/Sample-repository/master/Tools/say")!), 57 | name: "say", 58 | path: "Tools/say", 59 | sha: "1e3f1fd0bc1f65cf4701c217f4d1fd9a3cd50721", 60 | url: URL(string: "https://github.com/Palleas-opensource/Sample-repository/blob/master/Tools/say")! 61 | ) 62 | ]) 63 | let directory: Content = Fixture.FileForRepository.DirectoryInSampleRepository.decode()! 64 | 65 | XCTAssertEqual(directory, expected) 66 | } 67 | 68 | func testDecodedSubmodule() { 69 | let expected: Content = .file(Content.File( 70 | content: .submodule(url: "https://github.com/ReactiveCocoa/ReactiveSwift.git"), 71 | name: "ReactiveSwift", 72 | path: "Carthage/Checkouts/ReactiveSwift", 73 | sha: "e27ccdbf4ec36f154b60b91a0d7e0110c4e882cb", 74 | url: URL(string: "https://github.com/ReactiveCocoa/ReactiveSwift/tree/e27ccdbf4ec36f154b60b91a0d7e0110c4e882cb")! 75 | )) 76 | 77 | XCTAssertEqual(Fixture.FileForRepository.SubmoduleInTentacle.decode()!, expected) 78 | } 79 | 80 | func testDecodedSymlink() { 81 | let expected: Content = .file(Content.File( 82 | content: .symlink(target: "/usr/bin/say", downloadURL: URL(string: "https://raw.githubusercontent.com/Palleas-opensource/Sample-repository/master/Tools/say")), 83 | name: "say", 84 | path: "Tools/say", 85 | sha: "1e3f1fd0bc1f65cf4701c217f4d1fd9a3cd50721", 86 | url: URL(string: "https://github.com/Palleas-opensource/Sample-repository/blob/master/Tools/say")! 87 | )) 88 | 89 | XCTAssertEqual(Fixture.FileForRepository.SymlinkInSampleRepository.decode()!, expected) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/TentacleTests/EndpointTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EndpointTests.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2017-02-05. 6 | // Copyright © 2017 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tentacle 11 | 12 | class EndpointTests: XCTestCase { 13 | 14 | func testEndpointProvidesQueryItemsWhenNeeded() { 15 | let repository = Repository(owner: "palleas", name: "romain-pouclet.com") 16 | 17 | let endpoint = repository.content(atPath: "config.yml", atRef: "sample-branch") 18 | XCTAssertEqual([URLQueryItem(name: "ref", value: "sample-branch")], endpoint.queryItems) 19 | 20 | let endpointWithoutRef = repository.content(atPath: "config.yml", atRef: nil) 21 | XCTAssertEqual(0, endpointWithoutRef.queryItems.count) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Tests/TentacleTests/FileResponseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileResponseTests.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2016-12-24. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tentacle 11 | 12 | class FileResponseTests: XCTestCase { 13 | 14 | func testDecodedFileResponse() throws { 15 | #if SWIFT_PACKAGE 16 | let url = URL(fileURLWithPath: #file) 17 | .deletingLastPathComponent() 18 | .appendingPathComponent("Fixtures") 19 | .appendingPathComponent("create-file-sample-response.data") 20 | #else 21 | let url = Bundle(for: type(of: self)).url(forResource: "create-file-sample-response", withExtension: "data")! 22 | #endif 23 | 24 | let content = Content.file(Content.File( 25 | content: .file(size: 9, downloadURL: URL(string: "https://raw.githubusercontent.com/octocat/HelloWorld/master/notes/hello.txt")!), 26 | name: "hello.txt", 27 | path: "notes/hello.txt", 28 | sha: "95b966ae1c166bd92f8ae7d1c313e738c731dfc3", url: URL(string: "https://github.com/octocat/Hello-World/blob/master/notes/hello.txt")! 29 | )) 30 | 31 | let author = Commit.Author( 32 | date: DateFormatter.iso8601.date(from: "2014-11-07T22:01:45Z")!, 33 | name: "Scott Chacon", 34 | email: "schacon@gmail.com" 35 | ) 36 | 37 | let commit = Commit( 38 | sha: "7638417db6d59f3c431d3e1f261cc637155684cd", 39 | author: author, 40 | committer: author, 41 | message: "my commit message", 42 | url: URL(string: "https://github.com/octocat/Hello-World/git/commit/7638417db6d59f3c431d3e1f261cc637155684cd")!, 43 | parents: [ 44 | Commit.Parent( 45 | url: URL(string: "https://github.com/octocat/Hello-World/git/commit/1acc419d4d6a9ce985db7be48c6349a0475975b5")!, 46 | sha: "1acc419d4d6a9ce985db7be48c6349a0475975b5" 47 | ) 48 | ] 49 | ) 50 | let expected = FileResponse(content: content, commit: commit) 51 | let data = try Data(contentsOf: url) 52 | let decoder = JSONDecoder() 53 | decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601) 54 | 55 | let decoded = try decoder.decode(FileResponse.self, from: data) 56 | XCTAssertEqual(expected, decoded) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/TentacleTests/FileTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileTests.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2017-01-25. 6 | // Copyright © 2017 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | @testable import Tentacle 10 | import XCTest 11 | 12 | class FileTests: XCTestCase { 13 | 14 | func testFileEncoding() throws { 15 | let palleas = Author(name: "Romain Pouclet", email: "romain.pouclet@gmail.com") 16 | 17 | let file = File( 18 | message: "Added file", 19 | committer: palleas, 20 | author: palleas, 21 | content: "This is the content of my file".data(using: .utf8)!, 22 | branch: "master" 23 | ) 24 | 25 | let expectedFile = File( 26 | message: "Added file", 27 | committer: palleas, 28 | author: palleas, 29 | content: "This is the content of my file".data(using: .utf8)!, 30 | branch: "master" 31 | ) 32 | 33 | let encoder = JSONEncoder() 34 | let encodedFileContent = try encoder.encode(file) 35 | 36 | let decoder = JSONDecoder() 37 | let decodedFile = try decoder.decode(File.self, from: encodedFileContent) 38 | 39 | XCTAssertEqual(expectedFile, decodedFile) 40 | } 41 | 42 | func testFileEncodingWithoutOptionalArgs() throws { 43 | let file = File( 44 | message: "Added file", 45 | committer: nil, 46 | author: nil, 47 | content: "This is the content of my file".data(using: .utf8)!, 48 | branch: nil 49 | ) 50 | 51 | let expectedFile = File( 52 | message: "Added file", 53 | committer: nil, 54 | author: nil, 55 | content: "This is the content of my file".data(using: .utf8)!, 56 | branch: nil 57 | ) 58 | 59 | let encoder = JSONEncoder() 60 | let encodedFileContent = try encoder.encode(file) 61 | 62 | let decoder = JSONDecoder() 63 | let decodedFile = try decoder.decode(File.self, from: encodedFileContent) 64 | 65 | XCTAssertEqual(expectedFile, decodedFile) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixture.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Fixtures.swift 3 | // Tentacle 4 | // 5 | // Created by Matt Diephouse on 3/3/16. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import Tentacle 11 | 12 | /// A dummy class, so we can ask for the current bundle in Fixture.URL 13 | private class ImportedWithFixture { } 14 | 15 | protocol FixtureType { 16 | var url: URL { get } 17 | var contentType: String { get } 18 | } 19 | 20 | protocol EndpointFixtureType: FixtureType { 21 | associatedtype Value 22 | var request: Request { get } 23 | var page: UInt? { get } 24 | var perPage: UInt? { get } 25 | } 26 | 27 | extension FixtureType { 28 | /// The filename used for the local fixture, without an extension 29 | private func filename(withExtension ext: String) -> String { 30 | let components = URLComponents(url: url, resolvingAgainstBaseURL: false)! 31 | 32 | let path = (components.path as NSString) 33 | .pathComponents 34 | .dropFirst() 35 | .joined(separator: "-") 36 | 37 | let query = components.queryItems? 38 | .map { item in 39 | if let value = item.value { 40 | return "\(item.name)-\(value)" 41 | } else { 42 | return item.name 43 | } 44 | } 45 | .joined(separator: "-") 46 | 47 | if let query = query, query != "" { 48 | return "\(path).\(query).\(ext)" 49 | } 50 | return "\(path).\(ext)" 51 | } 52 | 53 | /// The filename used for the local fixture's data. 54 | var dataFilename: String { 55 | return filename(withExtension: Fixture.DataExtension) 56 | } 57 | 58 | /// The filename used for the local fixture's HTTP response. 59 | var responseFilename: String { 60 | return filename(withExtension: Fixture.ResponseExtension) 61 | } 62 | 63 | private func fileURL(withExtension ext: String) -> URL { 64 | let filename = self.filename(withExtension: ext) as NSString 65 | #if SWIFT_PACKAGE 66 | return URL(fileURLWithPath: #file) 67 | .deletingLastPathComponent() 68 | .appendingPathComponent("Fixtures") 69 | .appendingPathComponent(filename as String) 70 | #else 71 | let bundle = Bundle(for: ImportedWithFixture.self) 72 | return bundle.url(forResource: filename.deletingPathExtension, withExtension: filename.pathExtension)! 73 | #endif 74 | } 75 | 76 | /// The URL of the fixture's data within the test bundle. 77 | var dataFileURL: URL { 78 | return fileURL(withExtension: Fixture.DataExtension) 79 | } 80 | 81 | /// The URL of the fixture's HTTP response within the test bundle. 82 | var responseFileURL: URL { 83 | return fileURL(withExtension: Fixture.ResponseExtension) 84 | } 85 | 86 | /// The data from the endpoint. 87 | var data: Data { 88 | return (try! Data(contentsOf: dataFileURL)) 89 | } 90 | 91 | /// The HTTP response from the endpoint. 92 | var response: HTTPURLResponse { 93 | let data = try! Data(contentsOf: responseFileURL) 94 | let unarchiver = try! NSKeyedUnarchiver(forReadingFrom: data) 95 | unarchiver.requiresSecureCoding = false 96 | return try! unarchiver.decodeTopLevelObject(of: HTTPURLResponse.self, forKey: "root")! 97 | } 98 | } 99 | 100 | extension EndpointFixtureType { 101 | /// The URL of the fixture on the API. 102 | var url: URL { 103 | return URL(.dotCom, request, page: page, perPage: perPage) 104 | } 105 | 106 | /// Decode the fixture's JSON as an object of the returned type. 107 | func decode() -> Object? { 108 | let decoder = JSONDecoder() 109 | decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601) 110 | 111 | return try? decoder.decode(Object.self, from: data) 112 | } 113 | 114 | /// Decode the fixture's JSON as an array of objects of the returned type. 115 | func decodeList() -> [Object]? { 116 | let decoder = JSONDecoder() 117 | decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601) 118 | 119 | return try? decoder.decode([Object].self, from: data) 120 | } 121 | } 122 | 123 | extension Request: EndpointFixtureType { 124 | internal var contentType: String { 125 | return Client.APIContentType 126 | } 127 | 128 | internal var request: Request { 129 | return self 130 | } 131 | 132 | internal var page: UInt? { 133 | return nil 134 | } 135 | 136 | internal var perPage: UInt? { 137 | return nil 138 | } 139 | } 140 | 141 | struct Fixture { 142 | fileprivate static let DataExtension = "data" 143 | fileprivate static let ResponseExtension = "response" 144 | 145 | static var allFixtures: [FixtureType] = [ 146 | Repositories.Tentacle, 147 | LatestRelease.release, 148 | Release.Carthage0_15, 149 | Release.MDPSplitView1_0_2, 150 | Release.Nonexistent, 151 | Release.TagOnly, 152 | Release.Asset.MDPSplitView_framework_zip, 153 | Releases.Carthage[0], 154 | Releases.Carthage[1], 155 | Releases.Carthage[2], 156 | UserProfile.mdiep, 157 | UserProfile.test, 158 | IssuesInRepository.PalleasOpensource, 159 | IssueInRepository.Issue1InSampleRepository, 160 | CommentsOnIssue.CommentsOnIssueInSampleRepository, 161 | RepositoriesForUser.RepositoriesForPalleasOpensource, 162 | RepositoriesForOrganization.RepositoriesForRACCommunity, 163 | FileForRepository.ReadMeForSampleRepository, 164 | FileForRepository.SubmoduleInTentacle, 165 | FileForRepository.DirectoryInSampleRepository, 166 | FileForRepository.SymlinkInSampleRepository, 167 | BranchesForRepository.BranchesInReactiveTask, 168 | TreeForRepository.TreeInSampleRepository 169 | ] 170 | 171 | /// Returns the fixture for the given URL, or nil if no such fixture exists. 172 | static func fixtureForURL(_ url: URL) -> FixtureType? { 173 | return allFixtures.first { $0.url == url } 174 | } 175 | 176 | struct LatestRelease { 177 | static let release = Repository(owner: "mdiep", name: "MDPSplitView").latestRelease 178 | } 179 | 180 | struct Release { 181 | static let Carthage0_15 = Repository(owner: "Carthage", name: "Carthage").release(forTag: "0.15") 182 | static let MDPSplitView1_0_2 = Repository(owner: "mdiep", name: "MDPSplitView").release(forTag: "1.0.2") 183 | static let Nonexistent = Repository(owner: "mdiep", name: "NonExistent").release(forTag: "tag") 184 | static let TagOnly = Repository(owner: "torvalds", name: "linux").release(forTag: "v4.4") 185 | 186 | struct Asset: FixtureType { 187 | static let MDPSplitView_framework_zip = Asset("https://api.github.com/repos/mdiep/MDPSplitView/releases/assets/433845") 188 | 189 | let url: URL 190 | let contentType = Client.DownloadContentType 191 | 192 | init(_ URLString: String) { 193 | url = URL(string: URLString)! 194 | } 195 | } 196 | } 197 | 198 | struct Repositories { 199 | static let Tentacle = Repository(owner: "mdiep", name: "Tentacle").info 200 | } 201 | 202 | struct Releases: EndpointFixtureType { 203 | static let Carthage = [ 204 | Releases(Repository(owner: "Carthage", name: "Carthage").releases, 1, 30), 205 | Releases(Repository(owner: "Carthage", name: "Carthage").releases, 2, 30), 206 | Releases(Repository(owner: "Carthage", name: "Carthage").releases, 3, 30) 207 | ] 208 | 209 | let request: Request<[Tentacle.Release]> 210 | let page: UInt? 211 | let perPage: UInt? 212 | let contentType = Client.APIContentType 213 | 214 | init(_ request: Request<[Tentacle.Release]>, _ page: UInt?, _ perPage: UInt?) { 215 | self.request = request 216 | self.page = page 217 | self.perPage = perPage 218 | } 219 | } 220 | 221 | struct UserProfile { 222 | static let mdiep = User("mdiep").profile 223 | static let test = User("test").profile 224 | } 225 | 226 | struct IssuesInRepository { 227 | static let PalleasOpensource = Repository(owner: "Palleas-opensource", name: "Sample-repository").issues 228 | } 229 | 230 | struct IssueInRepository { 231 | static let Issue1InSampleRepository = Repository(owner: "Palleas-opensource", name: "Sample-repository").issue(id: 1) 232 | } 233 | 234 | struct CommentsOnIssue { 235 | static let CommentsOnIssueInSampleRepository = Repository(owner: "Palleas-Opensource", name: "Sample-repository").comments(onIssue: 1) 236 | } 237 | 238 | struct RepositoriesForUser { 239 | static let RepositoriesForPalleasOpensource = User("Palleas-Opensource").repositories 240 | } 241 | 242 | struct RepositoriesForOrganization { 243 | static let RepositoriesForRACCommunity = Organization("raccommunity").repositories 244 | } 245 | 246 | struct FileForRepository { 247 | static let ReadMeForSampleRepository = Repository(owner: "Palleas-opensource", name: "Sample-repository").content(atPath: "README.md") 248 | static let SubmoduleInTentacle = Repository(owner: "mdiep", name: "Tentacle").content(atPath: "Carthage/Checkouts/ReactiveSwift") 249 | static let DirectoryInSampleRepository = Repository(owner: "Palleas-opensource", name: "Sample-repository").content(atPath: "Tools") 250 | static let SymlinkInSampleRepository = Repository(owner: "Palleas-opensource", name: "Sample-repository").content(atPath: "Tools/say") 251 | } 252 | 253 | struct BranchesForRepository { 254 | static let BranchesInReactiveTask = Repository(owner: "Carthage", name: "ReactiveTask").branches 255 | } 256 | 257 | struct TreeForRepository { 258 | static let TreeInSampleRepository = Repository(owner: "Palleas-opensource", name: "Sample-repository") 259 | .tree(atRef: "0c0dfafa361836e11aedcbb95c1f05d3f654aef0", recursive: false) 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/create-file-sample-response.data: -------------------------------------------------------------------------------- 1 | { 2 | "content": { 3 | "name": "hello.txt", 4 | "path": "notes/hello.txt", 5 | "sha": "95b966ae1c166bd92f8ae7d1c313e738c731dfc3", 6 | "size": 9, 7 | "url": "https://api.github.com/repos/octocat/Hello-World/contents/notes/hello.txt", 8 | "html_url": "https://github.com/octocat/Hello-World/blob/master/notes/hello.txt", 9 | "git_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs/95b966ae1c166bd92f8ae7d1c313e738c731dfc3", 10 | "download_url": "https://raw.githubusercontent.com/octocat/HelloWorld/master/notes/hello.txt", 11 | "type": "file", 12 | "_links": { 13 | "self": "https://api.github.com/repos/octocat/Hello-World/contents/notes/hello.txt", 14 | "git": "https://api.github.com/repos/octocat/Hello-World/git/blobs/95b966ae1c166bd92f8ae7d1c313e738c731dfc3", 15 | "html": "https://github.com/octocat/Hello-World/blob/master/notes/hello.txt" 16 | } 17 | }, 18 | "commit": { 19 | "sha": "7638417db6d59f3c431d3e1f261cc637155684cd", 20 | "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/7638417db6d59f3c431d3e1f261cc637155684cd", 21 | "html_url": "https://github.com/octocat/Hello-World/git/commit/7638417db6d59f3c431d3e1f261cc637155684cd", 22 | "author": { 23 | "date": "2014-11-07T22:01:45Z", 24 | "name": "Scott Chacon", 25 | "email": "schacon@gmail.com" 26 | }, 27 | "committer": { 28 | "date": "2014-11-07T22:01:45Z", 29 | "name": "Scott Chacon", 30 | "email": "schacon@gmail.com" 31 | }, 32 | "message": "my commit message", 33 | "tree": { 34 | "url": "https://api.github.com/repos/octocat/Hello-World/git/trees/691272480426f78a0138979dd3ce63b77f706feb", 35 | "sha": "691272480426f78a0138979dd3ce63b77f706feb" 36 | }, 37 | "parents": [ 38 | { 39 | "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/1acc419d4d6a9ce985db7be48c6349a0475975b5", 40 | "html_url": "https://github.com/octocat/Hello-World/git/commit/1acc419d4d6a9ce985db7be48c6349a0475975b5", 41 | "sha": "1acc419d4d6a9ce985db7be48c6349a0475975b5" 42 | } 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/orgs-raccommunity-repos.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/orgs-raccommunity-repos.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-Carthage-Carthage-releases-tags-0.15.data: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 2698201, 3 | "draft" : false, 4 | "published_at" : "2016-02-26T17:53:30Z", 5 | "assets" : [ 6 | { 7 | "id" : 1358331, 8 | "uploader" : { 9 | "id" : 432536, 10 | "organizations_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/orgs", 11 | "received_events_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/received_events", 12 | "following_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/following{\/other_user}", 13 | "login" : "jspahrsummers", 14 | "avatar_url" : "https:\/\/avatars.githubusercontent.com\/u\/432536?v=4", 15 | "url" : "https:\/\/api.github.com\/users\/jspahrsummers", 16 | "node_id" : "MDQ6VXNlcjQzMjUzNg==", 17 | "subscriptions_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/subscriptions", 18 | "repos_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/repos", 19 | "type" : "User", 20 | "html_url" : "https:\/\/github.com\/jspahrsummers", 21 | "events_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/events{\/privacy}", 22 | "site_admin" : false, 23 | "starred_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/starred{\/owner}{\/repo}", 24 | "gists_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/gists{\/gist_id}", 25 | "gravatar_id" : "", 26 | "followers_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/followers" 27 | }, 28 | "label" : "", 29 | "state" : "uploaded", 30 | "created_at" : "2016-02-26T18:23:36Z", 31 | "content_type" : "application\/octet-stream", 32 | "url" : "https:\/\/api.github.com\/repos\/Carthage\/Carthage\/releases\/assets\/1358331", 33 | "node_id" : "MDEyOlJlbGVhc2VBc3NldDEzNTgzMzE=", 34 | "size" : 3691231, 35 | "updated_at" : "2016-02-26T18:23:37Z", 36 | "browser_download_url" : "https:\/\/github.com\/Carthage\/Carthage\/releases\/download\/0.15\/Carthage.pkg", 37 | "name" : "Carthage.pkg", 38 | "download_count" : 2366 39 | }, 40 | { 41 | "id" : 1358332, 42 | "uploader" : { 43 | "id" : 432536, 44 | "organizations_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/orgs", 45 | "received_events_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/received_events", 46 | "following_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/following{\/other_user}", 47 | "login" : "jspahrsummers", 48 | "avatar_url" : "https:\/\/avatars.githubusercontent.com\/u\/432536?v=4", 49 | "url" : "https:\/\/api.github.com\/users\/jspahrsummers", 50 | "node_id" : "MDQ6VXNlcjQzMjUzNg==", 51 | "subscriptions_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/subscriptions", 52 | "repos_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/repos", 53 | "type" : "User", 54 | "html_url" : "https:\/\/github.com\/jspahrsummers", 55 | "events_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/events{\/privacy}", 56 | "site_admin" : false, 57 | "starred_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/starred{\/owner}{\/repo}", 58 | "gists_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/gists{\/gist_id}", 59 | "gravatar_id" : "", 60 | "followers_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/followers" 61 | }, 62 | "label" : "", 63 | "state" : "uploaded", 64 | "created_at" : "2016-02-26T18:23:37Z", 65 | "content_type" : "application\/zip", 66 | "url" : "https:\/\/api.github.com\/repos\/Carthage\/Carthage\/releases\/assets\/1358332", 67 | "node_id" : "MDEyOlJlbGVhc2VBc3NldDEzNTgzMzI=", 68 | "size" : 3483682, 69 | "updated_at" : "2016-02-26T18:23:38Z", 70 | "browser_download_url" : "https:\/\/github.com\/Carthage\/Carthage\/releases\/download\/0.15\/CarthageKit.framework.zip", 71 | "name" : "CarthageKit.framework.zip", 72 | "download_count" : 47 73 | } 74 | ], 75 | "prerelease" : false, 76 | "author" : { 77 | "id" : 1302, 78 | "organizations_url" : "https:\/\/api.github.com\/users\/mdiep\/orgs", 79 | "received_events_url" : "https:\/\/api.github.com\/users\/mdiep\/received_events", 80 | "following_url" : "https:\/\/api.github.com\/users\/mdiep\/following{\/other_user}", 81 | "login" : "mdiep", 82 | "avatar_url" : "https:\/\/avatars.githubusercontent.com\/u\/1302?v=4", 83 | "url" : "https:\/\/api.github.com\/users\/mdiep", 84 | "node_id" : "MDQ6VXNlcjEzMDI=", 85 | "subscriptions_url" : "https:\/\/api.github.com\/users\/mdiep\/subscriptions", 86 | "repos_url" : "https:\/\/api.github.com\/users\/mdiep\/repos", 87 | "type" : "User", 88 | "html_url" : "https:\/\/github.com\/mdiep", 89 | "events_url" : "https:\/\/api.github.com\/users\/mdiep\/events{\/privacy}", 90 | "site_admin" : false, 91 | "starred_url" : "https:\/\/api.github.com\/users\/mdiep\/starred{\/owner}{\/repo}", 92 | "gists_url" : "https:\/\/api.github.com\/users\/mdiep\/gists{\/gist_id}", 93 | "gravatar_id" : "", 94 | "followers_url" : "https:\/\/api.github.com\/users\/mdiep\/followers" 95 | }, 96 | "created_at" : "2016-02-26T17:52:33Z", 97 | "zipball_url" : "https:\/\/api.github.com\/repos\/Carthage\/Carthage\/zipball\/0.15", 98 | "url" : "https:\/\/api.github.com\/repos\/Carthage\/Carthage\/releases\/2698201", 99 | "node_id" : "MDc6UmVsZWFzZTI2OTgyMDE=", 100 | "body" : "**Added**\n- A `CARTHAGE` environment variable so that Carthage builds can be detected (#1151). Thanks @Ashton-W!\n\n**Fixed**\n- Dependency resolution when building projects with complex dependency graphs (#1139). Thanks @erichoracek!\n- Added a timeout for `xcodebuild -showBuildSettings` (#1120). Thanks @mdiep!\n- Errors when running `git ls-tree` from the wrong directory (#1125, #1150). Thanks @bhargavg!\n\nThank you to @ikesyo, @mdiep, and @younata for improvements to the code base! Thank you to @daniel-beard for improvements to documentation! Thank you to @ikesyo, @NachoSoto, and @mdiep for reviewing pull requests!\n", 101 | "target_commitish" : "master", 102 | "tarball_url" : "https:\/\/api.github.com\/repos\/Carthage\/Carthage\/tarball\/0.15", 103 | "html_url" : "https:\/\/github.com\/Carthage\/Carthage\/releases\/tag\/0.15", 104 | "assets_url" : "https:\/\/api.github.com\/repos\/Carthage\/Carthage\/releases\/2698201\/assets", 105 | "upload_url" : "https:\/\/uploads.github.com\/repos\/Carthage\/Carthage\/releases\/2698201\/assets{?name,label}", 106 | "tag_name" : "0.15", 107 | "name" : "0.15: YOLOL" 108 | } -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-Carthage-Carthage-releases-tags-0.15.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/repos-Carthage-Carthage-releases-tags-0.15.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-Carthage-Carthage-releases.page-1-per_page-30.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/repos-Carthage-Carthage-releases.page-1-per_page-30.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-Carthage-Carthage-releases.page-2-per_page-30.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/repos-Carthage-Carthage-releases.page-2-per_page-30.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-Carthage-Carthage-releases.page-3-per_page-30.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/repos-Carthage-Carthage-releases.page-3-per_page-30.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-Carthage-ReactiveTask-branches.data: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name" : "debuggin", 4 | "commit" : { 5 | "sha" : "117775803ff583c467dac3cd2c923b8d3f7d1869", 6 | "url" : "https:\/\/api.github.com\/repos\/Carthage\/ReactiveTask\/commits\/117775803ff583c467dac3cd2c923b8d3f7d1869" 7 | }, 8 | "protected" : false 9 | }, 10 | { 11 | "name" : "master", 12 | "commit" : { 13 | "sha" : "e1396a56055812234e97aeda78731d7228e0bbc7", 14 | "url" : "https:\/\/api.github.com\/repos\/Carthage\/ReactiveTask\/commits\/e1396a56055812234e97aeda78731d7228e0bbc7" 15 | }, 16 | "protected" : false 17 | }, 18 | { 19 | "name" : "playground", 20 | "commit" : { 21 | "sha" : "131709d54e1157699e44300cb9b9f8d22f2807e7", 22 | "url" : "https:\/\/api.github.com\/repos\/Carthage\/ReactiveTask\/commits\/131709d54e1157699e44300cb9b9f8d22f2807e7" 23 | }, 24 | "protected" : false 25 | }, 26 | { 27 | "name" : "release-0.17.0", 28 | "commit" : { 29 | "sha" : "0f6162a44d56b3e2fc3e42d20e77b8b09bd5e00a", 30 | "url" : "https:\/\/api.github.com\/repos\/Carthage\/ReactiveTask\/commits\/0f6162a44d56b3e2fc3e42d20e77b8b09bd5e00a" 31 | }, 32 | "protected" : false 33 | } 34 | ] -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-Carthage-ReactiveTask-branches.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/repos-Carthage-ReactiveTask-branches.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-Palleas-Opensource-Sample-repository-issues-1-comments.data: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id" : 235455442, 4 | "author_association" : "OWNER", 5 | "body" : "I know right?!\n", 6 | "reactions" : { 7 | "-1" : 0, 8 | "hooray" : 0, 9 | "+1" : 0, 10 | "rocket" : 0, 11 | "laugh" : 0, 12 | "heart" : 0, 13 | "confused" : 0, 14 | "eyes" : 0, 15 | "total_count" : 0, 16 | "url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/issues\/comments\/235455442\/reactions" 17 | }, 18 | "issue_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/issues\/1", 19 | "created_at" : "2016-07-27T01:28:21Z", 20 | "url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/issues\/comments\/235455442", 21 | "node_id" : "MDEyOklzc3VlQ29tbWVudDIzNTQ1NTQ0Mg==", 22 | "html_url" : "https:\/\/github.com\/Palleas-opensource\/Sample-repository\/issues\/1#issuecomment-235455442", 23 | "updated_at" : "2016-07-27T01:28:21Z", 24 | "performed_via_github_app" : null, 25 | "user" : { 26 | "id" : 15802020, 27 | "organizations_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/orgs", 28 | "received_events_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/received_events", 29 | "following_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/following{\/other_user}", 30 | "login" : "Palleas-opensource", 31 | "avatar_url" : "https:\/\/avatars.githubusercontent.com\/u\/15802020?v=4", 32 | "url" : "https:\/\/api.github.com\/users\/Palleas-opensource", 33 | "node_id" : "MDQ6VXNlcjE1ODAyMDIw", 34 | "subscriptions_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/subscriptions", 35 | "repos_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/repos", 36 | "type" : "User", 37 | "html_url" : "https:\/\/github.com\/Palleas-opensource", 38 | "events_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/events{\/privacy}", 39 | "site_admin" : false, 40 | "starred_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/starred{\/owner}{\/repo}", 41 | "gists_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/gists{\/gist_id}", 42 | "gravatar_id" : "", 43 | "followers_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/followers" 44 | } 45 | }, 46 | { 47 | "id" : 235455603, 48 | "author_association" : "CONTRIBUTOR", 49 | "body" : "👍 Good idea to say stuff like that on internet!\n", 50 | "reactions" : { 51 | "-1" : 0, 52 | "hooray" : 0, 53 | "+1" : 0, 54 | "rocket" : 0, 55 | "laugh" : 0, 56 | "heart" : 0, 57 | "confused" : 0, 58 | "eyes" : 0, 59 | "total_count" : 0, 60 | "url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/issues\/comments\/235455603\/reactions" 61 | }, 62 | "issue_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/issues\/1", 63 | "created_at" : "2016-07-27T01:29:31Z", 64 | "url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/issues\/comments\/235455603", 65 | "node_id" : "MDEyOklzc3VlQ29tbWVudDIzNTQ1NTYwMw==", 66 | "html_url" : "https:\/\/github.com\/Palleas-opensource\/Sample-repository\/issues\/1#issuecomment-235455603", 67 | "updated_at" : "2016-07-27T01:29:31Z", 68 | "performed_via_github_app" : null, 69 | "user" : { 70 | "id" : 48797, 71 | "organizations_url" : "https:\/\/api.github.com\/users\/Palleas\/orgs", 72 | "received_events_url" : "https:\/\/api.github.com\/users\/Palleas\/received_events", 73 | "following_url" : "https:\/\/api.github.com\/users\/Palleas\/following{\/other_user}", 74 | "login" : "Palleas", 75 | "avatar_url" : "https:\/\/avatars.githubusercontent.com\/u\/48797?v=4", 76 | "url" : "https:\/\/api.github.com\/users\/Palleas", 77 | "node_id" : "MDQ6VXNlcjQ4Nzk3", 78 | "subscriptions_url" : "https:\/\/api.github.com\/users\/Palleas\/subscriptions", 79 | "repos_url" : "https:\/\/api.github.com\/users\/Palleas\/repos", 80 | "type" : "User", 81 | "html_url" : "https:\/\/github.com\/Palleas", 82 | "events_url" : "https:\/\/api.github.com\/users\/Palleas\/events{\/privacy}", 83 | "site_admin" : false, 84 | "starred_url" : "https:\/\/api.github.com\/users\/Palleas\/starred{\/owner}{\/repo}", 85 | "gists_url" : "https:\/\/api.github.com\/users\/Palleas\/gists{\/gist_id}", 86 | "gravatar_id" : "", 87 | "followers_url" : "https:\/\/api.github.com\/users\/Palleas\/followers" 88 | } 89 | } 90 | ] -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-Palleas-Opensource-Sample-repository-issues-1-comments.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/repos-Palleas-Opensource-Sample-repository-issues-1-comments.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-Palleas-opensource-Sample-repository-contents-README.md.data: -------------------------------------------------------------------------------- 1 | { 2 | "encoding" : "base64", 3 | "download_url" : "https:\/\/raw.githubusercontent.com\/Palleas-opensource\/Sample-repository\/master\/README.md", 4 | "url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/contents\/README.md?ref=master", 5 | "path" : "README.md", 6 | "size" : 19, 7 | "type" : "file", 8 | "git_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/git\/blobs\/28ec72028c4ae47de689964a23ebb223f10cfe80", 9 | "html_url" : "https:\/\/github.com\/Palleas-opensource\/Sample-repository\/blob\/master\/README.md", 10 | "_links" : { 11 | "git" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/git\/blobs\/28ec72028c4ae47de689964a23ebb223f10cfe80", 12 | "html" : "https:\/\/github.com\/Palleas-opensource\/Sample-repository\/blob\/master\/README.md", 13 | "self" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/contents\/README.md?ref=master" 14 | }, 15 | "sha" : "28ec72028c4ae47de689964a23ebb223f10cfe80", 16 | "name" : "README.md", 17 | "content" : "IyBTYW1wbGUtcmVwb3NpdG9yeQ==\n" 18 | } -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-Palleas-opensource-Sample-repository-contents-README.md.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/repos-Palleas-opensource-Sample-repository-contents-README.md.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-Palleas-opensource-Sample-repository-contents-Tools-say.data: -------------------------------------------------------------------------------- 1 | { 2 | "target" : "\/usr\/bin\/say", 3 | "git_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/git\/blobs\/1e3f1fd0bc1f65cf4701c217f4d1fd9a3cd50721", 4 | "_links" : { 5 | "git" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/git\/blobs\/1e3f1fd0bc1f65cf4701c217f4d1fd9a3cd50721", 6 | "html" : "https:\/\/github.com\/Palleas-opensource\/Sample-repository\/blob\/master\/Tools\/say", 7 | "self" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/contents\/Tools\/say?ref=master" 8 | }, 9 | "html_url" : "https:\/\/github.com\/Palleas-opensource\/Sample-repository\/blob\/master\/Tools\/say", 10 | "download_url" : "https:\/\/raw.githubusercontent.com\/Palleas-opensource\/Sample-repository\/master\/Tools\/say", 11 | "size" : 12, 12 | "sha" : "1e3f1fd0bc1f65cf4701c217f4d1fd9a3cd50721", 13 | "path" : "Tools\/say", 14 | "type" : "symlink", 15 | "name" : "say", 16 | "url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/contents\/Tools\/say?ref=master" 17 | } -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-Palleas-opensource-Sample-repository-contents-Tools-say.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/repos-Palleas-opensource-Sample-repository-contents-Tools-say.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-Palleas-opensource-Sample-repository-contents-Tools.data: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_links" : { 4 | "git" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/git\/trees\/5bfad2b3f8e483b6b173d8aaff19597e84626f15", 5 | "html" : "https:\/\/github.com\/Palleas-opensource\/Sample-repository\/tree\/master\/Tools\/Directory", 6 | "self" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/contents\/Tools\/Directory?ref=master" 7 | }, 8 | "git_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/git\/trees\/5bfad2b3f8e483b6b173d8aaff19597e84626f15", 9 | "html_url" : "https:\/\/github.com\/Palleas-opensource\/Sample-repository\/tree\/master\/Tools\/Directory", 10 | "download_url" : null, 11 | "size" : 0, 12 | "sha" : "5bfad2b3f8e483b6b173d8aaff19597e84626f15", 13 | "path" : "Tools\/Directory", 14 | "type" : "dir", 15 | "name" : "Directory", 16 | "url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/contents\/Tools\/Directory?ref=master" 17 | }, 18 | { 19 | "_links" : { 20 | "git" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/git\/blobs\/c3eb8708a0a5aaa4f685aab24ef6403fbfd28efc", 21 | "html" : "https:\/\/github.com\/Palleas-opensource\/Sample-repository\/blob\/master\/Tools\/README.markdown", 22 | "self" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/contents\/Tools\/README.markdown?ref=master" 23 | }, 24 | "git_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/git\/blobs\/c3eb8708a0a5aaa4f685aab24ef6403fbfd28efc", 25 | "html_url" : "https:\/\/github.com\/Palleas-opensource\/Sample-repository\/blob\/master\/Tools\/README.markdown", 26 | "download_url" : "https:\/\/raw.githubusercontent.com\/Palleas-opensource\/Sample-repository\/master\/Tools\/README.markdown", 27 | "size" : 18, 28 | "sha" : "c3eb8708a0a5aaa4f685aab24ef6403fbfd28efc", 29 | "path" : "Tools\/README.markdown", 30 | "type" : "file", 31 | "name" : "README.markdown", 32 | "url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/contents\/Tools\/README.markdown?ref=master" 33 | }, 34 | { 35 | "_links" : { 36 | "git" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/git\/trees\/7a84505a3c553fd8e2879cfa63753b0cd212feb8", 37 | "html" : "https:\/\/github.com\/mdiep\/Tentacle\/tree\/7a84505a3c553fd8e2879cfa63753b0cd212feb8", 38 | "self" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/contents\/Tools\/Tentacle?ref=master" 39 | }, 40 | "git_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/git\/trees\/7a84505a3c553fd8e2879cfa63753b0cd212feb8", 41 | "html_url" : "https:\/\/github.com\/mdiep\/Tentacle\/tree\/7a84505a3c553fd8e2879cfa63753b0cd212feb8", 42 | "download_url" : null, 43 | "size" : 0, 44 | "sha" : "7a84505a3c553fd8e2879cfa63753b0cd212feb8", 45 | "path" : "Tools\/Tentacle", 46 | "type" : "file", 47 | "name" : "Tentacle", 48 | "url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/contents\/Tools\/Tentacle?ref=master" 49 | }, 50 | { 51 | "_links" : { 52 | "git" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/git\/blobs\/1e3f1fd0bc1f65cf4701c217f4d1fd9a3cd50721", 53 | "html" : "https:\/\/github.com\/Palleas-opensource\/Sample-repository\/blob\/master\/Tools\/say", 54 | "self" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/contents\/Tools\/say?ref=master" 55 | }, 56 | "git_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/git\/blobs\/1e3f1fd0bc1f65cf4701c217f4d1fd9a3cd50721", 57 | "html_url" : "https:\/\/github.com\/Palleas-opensource\/Sample-repository\/blob\/master\/Tools\/say", 58 | "download_url" : "https:\/\/raw.githubusercontent.com\/Palleas-opensource\/Sample-repository\/master\/Tools\/say", 59 | "size" : 12, 60 | "sha" : "1e3f1fd0bc1f65cf4701c217f4d1fd9a3cd50721", 61 | "path" : "Tools\/say", 62 | "type" : "symlink", 63 | "name" : "say", 64 | "url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/contents\/Tools\/say?ref=master" 65 | } 66 | ] -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-Palleas-opensource-Sample-repository-contents-Tools.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/repos-Palleas-opensource-Sample-repository-contents-Tools.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-Palleas-opensource-Sample-repository-contents-say.data: -------------------------------------------------------------------------------- 1 | { 2 | "target" : "\/usr\/bin\/say", 3 | "git_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/git\/blobs\/1e3f1fd0bc1f65cf4701c217f4d1fd9a3cd50721", 4 | "_links" : { 5 | "git" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/git\/blobs\/1e3f1fd0bc1f65cf4701c217f4d1fd9a3cd50721", 6 | "html" : "https:\/\/github.com\/Palleas-opensource\/Sample-repository\/blob\/master\/say", 7 | "self" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/contents\/say?ref=master" 8 | }, 9 | "html_url" : "https:\/\/github.com\/Palleas-opensource\/Sample-repository\/blob\/master\/say", 10 | "download_url" : "https:\/\/raw.githubusercontent.com\/Palleas-opensource\/Sample-repository\/master\/say", 11 | "size" : 12, 12 | "sha" : "1e3f1fd0bc1f65cf4701c217f4d1fd9a3cd50721", 13 | "path" : "say", 14 | "type" : "symlink", 15 | "name" : "say", 16 | "url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/contents\/say?ref=master" 17 | } -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-Palleas-opensource-Sample-repository-contents-say.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/repos-Palleas-opensource-Sample-repository-contents-say.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-Palleas-opensource-Sample-repository-git-trees-0c0dfafa361836e11aedcbb95c1f05d3f654aef0.data: -------------------------------------------------------------------------------- 1 | { 2 | "tree" : [ 3 | { 4 | "path" : "Directory", 5 | "url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/git\/trees\/5bfad2b3f8e483b6b173d8aaff19597e84626f15", 6 | "type" : "tree", 7 | "mode" : "040000", 8 | "sha" : "5bfad2b3f8e483b6b173d8aaff19597e84626f15" 9 | }, 10 | { 11 | "path" : "README.markdown", 12 | "size" : 18, 13 | "url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/git\/blobs\/c3eb8708a0a5aaa4f685aab24ef6403fbfd28efc", 14 | "type" : "blob", 15 | "mode" : "100644", 16 | "sha" : "c3eb8708a0a5aaa4f685aab24ef6403fbfd28efc" 17 | }, 18 | { 19 | "path" : "Tentacle", 20 | "type" : "commit", 21 | "mode" : "160000", 22 | "sha" : "7a84505a3c553fd8e2879cfa63753b0cd212feb8" 23 | }, 24 | { 25 | "path" : "say", 26 | "size" : 12, 27 | "url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/git\/blobs\/1e3f1fd0bc1f65cf4701c217f4d1fd9a3cd50721", 28 | "type" : "blob", 29 | "mode" : "120000", 30 | "sha" : "1e3f1fd0bc1f65cf4701c217f4d1fd9a3cd50721" 31 | } 32 | ], 33 | "url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/git\/trees\/0c0dfafa361836e11aedcbb95c1f05d3f654aef0", 34 | "truncated" : false, 35 | "sha" : "0c0dfafa361836e11aedcbb95c1f05d3f654aef0" 36 | } -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-Palleas-opensource-Sample-repository-git-trees-0c0dfafa361836e11aedcbb95c1f05d3f654aef0.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/repos-Palleas-opensource-Sample-repository-git-trees-0c0dfafa361836e11aedcbb95c1f05d3f654aef0.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-Palleas-opensource-Sample-repository-issues-1.data: -------------------------------------------------------------------------------- 1 | { 2 | "labels" : [ 3 | { 4 | "default" : true, 5 | "color" : "ee0701", 6 | "id" : 381957828, 7 | "node_id" : "MDU6TGFiZWwzODE5NTc4Mjg=", 8 | "description" : null, 9 | "name" : "bug", 10 | "url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/labels\/bug" 11 | }, 12 | { 13 | "default" : true, 14 | "color" : "cccccc", 15 | "id" : 381957829, 16 | "node_id" : "MDU6TGFiZWwzODE5NTc4Mjk=", 17 | "description" : null, 18 | "name" : "duplicate", 19 | "url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/labels\/duplicate" 20 | }, 21 | { 22 | "default" : true, 23 | "color" : "84b6eb", 24 | "id" : 381957830, 25 | "node_id" : "MDU6TGFiZWwzODE5NTc4MzA=", 26 | "description" : null, 27 | "name" : "enhancement", 28 | "url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/labels\/enhancement" 29 | } 30 | ], 31 | "locked" : false, 32 | "milestone" : { 33 | "id" : 1881390, 34 | "description" : "That'd be cool.", 35 | "open_issues" : 1, 36 | "state" : "open", 37 | "created_at" : "2016-07-13T16:56:48Z", 38 | "labels_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/milestones\/1\/labels", 39 | "url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/milestones\/1", 40 | "node_id" : "MDk6TWlsZXN0b25lMTg4MTM5MA==", 41 | "closed_issues" : 0, 42 | "title" : "Release this app", 43 | "creator" : { 44 | "id" : 15802020, 45 | "organizations_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/orgs", 46 | "received_events_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/received_events", 47 | "following_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/following{\/other_user}", 48 | "login" : "Palleas-opensource", 49 | "avatar_url" : "https:\/\/avatars.githubusercontent.com\/u\/15802020?v=4", 50 | "url" : "https:\/\/api.github.com\/users\/Palleas-opensource", 51 | "node_id" : "MDQ6VXNlcjE1ODAyMDIw", 52 | "subscriptions_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/subscriptions", 53 | "repos_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/repos", 54 | "type" : "User", 55 | "html_url" : "https:\/\/github.com\/Palleas-opensource", 56 | "events_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/events{\/privacy}", 57 | "site_admin" : false, 58 | "starred_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/starred{\/owner}{\/repo}", 59 | "gists_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/gists{\/gist_id}", 60 | "gravatar_id" : "", 61 | "followers_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/followers" 62 | }, 63 | "html_url" : "https:\/\/github.com\/Palleas-opensource\/Sample-repository\/milestone\/1", 64 | "number" : 1, 65 | "updated_at" : "2016-07-13T16:56:57Z", 66 | "due_on" : "2016-07-25T07:00:00Z", 67 | "closed_at" : null 68 | }, 69 | "title" : "This issue is open", 70 | "url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/issues\/1", 71 | "events_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/issues\/1\/events", 72 | "reactions" : { 73 | "-1" : 0, 74 | "hooray" : 0, 75 | "+1" : 0, 76 | "rocket" : 0, 77 | "laugh" : 0, 78 | "heart" : 0, 79 | "confused" : 0, 80 | "eyes" : 0, 81 | "total_count" : 0, 82 | "url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/issues\/1\/reactions" 83 | }, 84 | "updated_at" : "2016-07-27T01:29:31Z", 85 | "comments_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/issues\/1\/comments", 86 | "assignee" : { 87 | "id" : 15802020, 88 | "organizations_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/orgs", 89 | "received_events_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/received_events", 90 | "following_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/following{\/other_user}", 91 | "login" : "Palleas-opensource", 92 | "avatar_url" : "https:\/\/avatars.githubusercontent.com\/u\/15802020?v=4", 93 | "url" : "https:\/\/api.github.com\/users\/Palleas-opensource", 94 | "node_id" : "MDQ6VXNlcjE1ODAyMDIw", 95 | "subscriptions_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/subscriptions", 96 | "repos_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/repos", 97 | "type" : "User", 98 | "html_url" : "https:\/\/github.com\/Palleas-opensource", 99 | "events_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/events{\/privacy}", 100 | "site_admin" : false, 101 | "starred_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/starred{\/owner}{\/repo}", 102 | "gists_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/gists{\/gist_id}", 103 | "gravatar_id" : "", 104 | "followers_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/followers" 105 | }, 106 | "state" : "open", 107 | "body" : "Issues are pretty cool.\n", 108 | "id" : 156633109, 109 | "number" : 1, 110 | "repository_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository", 111 | "user" : { 112 | "id" : 15802020, 113 | "organizations_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/orgs", 114 | "received_events_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/received_events", 115 | "following_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/following{\/other_user}", 116 | "login" : "Palleas-opensource", 117 | "avatar_url" : "https:\/\/avatars.githubusercontent.com\/u\/15802020?v=4", 118 | "url" : "https:\/\/api.github.com\/users\/Palleas-opensource", 119 | "node_id" : "MDQ6VXNlcjE1ODAyMDIw", 120 | "subscriptions_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/subscriptions", 121 | "repos_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/repos", 122 | "type" : "User", 123 | "html_url" : "https:\/\/github.com\/Palleas-opensource", 124 | "events_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/events{\/privacy}", 125 | "site_admin" : false, 126 | "starred_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/starred{\/owner}{\/repo}", 127 | "gists_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/gists{\/gist_id}", 128 | "gravatar_id" : "", 129 | "followers_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/followers" 130 | }, 131 | "closed_at" : null, 132 | "active_lock_reason" : null, 133 | "timeline_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/issues\/1\/timeline", 134 | "state_reason" : null, 135 | "closed_by" : null, 136 | "labels_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/issues\/1\/labels{\/name}", 137 | "assignees" : [ 138 | { 139 | "id" : 15802020, 140 | "organizations_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/orgs", 141 | "received_events_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/received_events", 142 | "following_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/following{\/other_user}", 143 | "login" : "Palleas-opensource", 144 | "avatar_url" : "https:\/\/avatars.githubusercontent.com\/u\/15802020?v=4", 145 | "url" : "https:\/\/api.github.com\/users\/Palleas-opensource", 146 | "node_id" : "MDQ6VXNlcjE1ODAyMDIw", 147 | "subscriptions_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/subscriptions", 148 | "repos_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/repos", 149 | "type" : "User", 150 | "html_url" : "https:\/\/github.com\/Palleas-opensource", 151 | "events_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/events{\/privacy}", 152 | "site_admin" : false, 153 | "starred_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/starred{\/owner}{\/repo}", 154 | "gists_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/gists{\/gist_id}", 155 | "gravatar_id" : "", 156 | "followers_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/followers" 157 | } 158 | ], 159 | "comments" : 2, 160 | "created_at" : "2016-05-24T23:38:39Z", 161 | "node_id" : "MDU6SXNzdWUxNTY2MzMxMDk=", 162 | "author_association" : "OWNER", 163 | "performed_via_github_app" : null, 164 | "html_url" : "https:\/\/github.com\/Palleas-opensource\/Sample-repository\/issues\/1" 165 | } -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-Palleas-opensource-Sample-repository-issues-1.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/repos-Palleas-opensource-Sample-repository-issues-1.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-Palleas-opensource-Sample-repository-issues.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/repos-Palleas-opensource-Sample-repository-issues.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-mdiep-MDPSplitView-releases-assets-433845.data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/repos-mdiep-MDPSplitView-releases-assets-433845.data -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-mdiep-MDPSplitView-releases-assets-433845.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/repos-mdiep-MDPSplitView-releases-assets-433845.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-mdiep-MDPSplitView-releases-latest.data: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 961251, 3 | "draft" : false, 4 | "published_at" : "2015-02-17T21:35:40Z", 5 | "assets" : [ 6 | { 7 | "id" : 433845, 8 | "uploader" : { 9 | "id" : 432536, 10 | "organizations_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/orgs", 11 | "received_events_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/received_events", 12 | "following_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/following{\/other_user}", 13 | "login" : "jspahrsummers", 14 | "avatar_url" : "https:\/\/avatars.githubusercontent.com\/u\/432536?v=4", 15 | "url" : "https:\/\/api.github.com\/users\/jspahrsummers", 16 | "node_id" : "MDQ6VXNlcjQzMjUzNg==", 17 | "subscriptions_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/subscriptions", 18 | "repos_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/repos", 19 | "type" : "User", 20 | "html_url" : "https:\/\/github.com\/jspahrsummers", 21 | "events_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/events{\/privacy}", 22 | "site_admin" : false, 23 | "starred_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/starred{\/owner}{\/repo}", 24 | "gists_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/gists{\/gist_id}", 25 | "gravatar_id" : "", 26 | "followers_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/followers" 27 | }, 28 | "label" : null, 29 | "state" : "uploaded", 30 | "created_at" : "2015-02-20T22:44:56Z", 31 | "content_type" : "application\/zip", 32 | "url" : "https:\/\/api.github.com\/repos\/mdiep\/MDPSplitView\/releases\/assets\/433845", 33 | "node_id" : "MDEyOlJlbGVhc2VBc3NldDQzMzg0NQ==", 34 | "size" : 11784, 35 | "updated_at" : "2015-02-20T22:44:57Z", 36 | "browser_download_url" : "https:\/\/github.com\/mdiep\/MDPSplitView\/releases\/download\/1.0.2\/MDPSplitView.framework.zip", 37 | "name" : "MDPSplitView.framework.zip", 38 | "download_count" : 176 39 | } 40 | ], 41 | "prerelease" : false, 42 | "author" : { 43 | "id" : 1302, 44 | "organizations_url" : "https:\/\/api.github.com\/users\/mdiep\/orgs", 45 | "received_events_url" : "https:\/\/api.github.com\/users\/mdiep\/received_events", 46 | "following_url" : "https:\/\/api.github.com\/users\/mdiep\/following{\/other_user}", 47 | "login" : "mdiep", 48 | "avatar_url" : "https:\/\/avatars.githubusercontent.com\/u\/1302?v=4", 49 | "url" : "https:\/\/api.github.com\/users\/mdiep", 50 | "node_id" : "MDQ6VXNlcjEzMDI=", 51 | "subscriptions_url" : "https:\/\/api.github.com\/users\/mdiep\/subscriptions", 52 | "repos_url" : "https:\/\/api.github.com\/users\/mdiep\/repos", 53 | "type" : "User", 54 | "html_url" : "https:\/\/github.com\/mdiep", 55 | "events_url" : "https:\/\/api.github.com\/users\/mdiep\/events{\/privacy}", 56 | "site_admin" : false, 57 | "starred_url" : "https:\/\/api.github.com\/users\/mdiep\/starred{\/owner}{\/repo}", 58 | "gists_url" : "https:\/\/api.github.com\/users\/mdiep\/gists{\/gist_id}", 59 | "gravatar_id" : "", 60 | "followers_url" : "https:\/\/api.github.com\/users\/mdiep\/followers" 61 | }, 62 | "created_at" : "2015-02-17T21:34:52Z", 63 | "zipball_url" : "https:\/\/api.github.com\/repos\/mdiep\/MDPSplitView\/zipball\/1.0.2", 64 | "url" : "https:\/\/api.github.com\/repos\/mdiep\/MDPSplitView\/releases\/961251", 65 | "node_id" : "MDc6UmVsZWFzZTk2MTI1MQ==", 66 | "body" : "This release fixes up the bugs introduced in 1.0.1 (#15).\n", 67 | "target_commitish" : "master", 68 | "tarball_url" : "https:\/\/api.github.com\/repos\/mdiep\/MDPSplitView\/tarball\/1.0.2", 69 | "html_url" : "https:\/\/github.com\/mdiep\/MDPSplitView\/releases\/tag\/1.0.2", 70 | "assets_url" : "https:\/\/api.github.com\/repos\/mdiep\/MDPSplitView\/releases\/961251\/assets", 71 | "upload_url" : "https:\/\/uploads.github.com\/repos\/mdiep\/MDPSplitView\/releases\/961251\/assets{?name,label}", 72 | "tag_name" : "1.0.2", 73 | "name" : "1.0.2" 74 | } -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-mdiep-MDPSplitView-releases-latest.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/repos-mdiep-MDPSplitView-releases-latest.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-mdiep-MDPSplitView-releases-tags-1.0.2.data: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 961251, 3 | "draft" : false, 4 | "published_at" : "2015-02-17T21:35:40Z", 5 | "assets" : [ 6 | { 7 | "id" : 433845, 8 | "uploader" : { 9 | "id" : 432536, 10 | "organizations_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/orgs", 11 | "received_events_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/received_events", 12 | "following_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/following{\/other_user}", 13 | "login" : "jspahrsummers", 14 | "avatar_url" : "https:\/\/avatars.githubusercontent.com\/u\/432536?v=4", 15 | "url" : "https:\/\/api.github.com\/users\/jspahrsummers", 16 | "node_id" : "MDQ6VXNlcjQzMjUzNg==", 17 | "subscriptions_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/subscriptions", 18 | "repos_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/repos", 19 | "type" : "User", 20 | "html_url" : "https:\/\/github.com\/jspahrsummers", 21 | "events_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/events{\/privacy}", 22 | "site_admin" : false, 23 | "starred_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/starred{\/owner}{\/repo}", 24 | "gists_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/gists{\/gist_id}", 25 | "gravatar_id" : "", 26 | "followers_url" : "https:\/\/api.github.com\/users\/jspahrsummers\/followers" 27 | }, 28 | "label" : null, 29 | "state" : "uploaded", 30 | "created_at" : "2015-02-20T22:44:56Z", 31 | "content_type" : "application\/zip", 32 | "url" : "https:\/\/api.github.com\/repos\/mdiep\/MDPSplitView\/releases\/assets\/433845", 33 | "node_id" : "MDEyOlJlbGVhc2VBc3NldDQzMzg0NQ==", 34 | "size" : 11784, 35 | "updated_at" : "2015-02-20T22:44:57Z", 36 | "browser_download_url" : "https:\/\/github.com\/mdiep\/MDPSplitView\/releases\/download\/1.0.2\/MDPSplitView.framework.zip", 37 | "name" : "MDPSplitView.framework.zip", 38 | "download_count" : 176 39 | } 40 | ], 41 | "prerelease" : false, 42 | "author" : { 43 | "id" : 1302, 44 | "organizations_url" : "https:\/\/api.github.com\/users\/mdiep\/orgs", 45 | "received_events_url" : "https:\/\/api.github.com\/users\/mdiep\/received_events", 46 | "following_url" : "https:\/\/api.github.com\/users\/mdiep\/following{\/other_user}", 47 | "login" : "mdiep", 48 | "avatar_url" : "https:\/\/avatars.githubusercontent.com\/u\/1302?v=4", 49 | "url" : "https:\/\/api.github.com\/users\/mdiep", 50 | "node_id" : "MDQ6VXNlcjEzMDI=", 51 | "subscriptions_url" : "https:\/\/api.github.com\/users\/mdiep\/subscriptions", 52 | "repos_url" : "https:\/\/api.github.com\/users\/mdiep\/repos", 53 | "type" : "User", 54 | "html_url" : "https:\/\/github.com\/mdiep", 55 | "events_url" : "https:\/\/api.github.com\/users\/mdiep\/events{\/privacy}", 56 | "site_admin" : false, 57 | "starred_url" : "https:\/\/api.github.com\/users\/mdiep\/starred{\/owner}{\/repo}", 58 | "gists_url" : "https:\/\/api.github.com\/users\/mdiep\/gists{\/gist_id}", 59 | "gravatar_id" : "", 60 | "followers_url" : "https:\/\/api.github.com\/users\/mdiep\/followers" 61 | }, 62 | "created_at" : "2015-02-17T21:34:52Z", 63 | "zipball_url" : "https:\/\/api.github.com\/repos\/mdiep\/MDPSplitView\/zipball\/1.0.2", 64 | "url" : "https:\/\/api.github.com\/repos\/mdiep\/MDPSplitView\/releases\/961251", 65 | "node_id" : "MDc6UmVsZWFzZTk2MTI1MQ==", 66 | "body" : "This release fixes up the bugs introduced in 1.0.1 (#15).\n", 67 | "target_commitish" : "master", 68 | "tarball_url" : "https:\/\/api.github.com\/repos\/mdiep\/MDPSplitView\/tarball\/1.0.2", 69 | "html_url" : "https:\/\/github.com\/mdiep\/MDPSplitView\/releases\/tag\/1.0.2", 70 | "assets_url" : "https:\/\/api.github.com\/repos\/mdiep\/MDPSplitView\/releases\/961251\/assets", 71 | "upload_url" : "https:\/\/uploads.github.com\/repos\/mdiep\/MDPSplitView\/releases\/961251\/assets{?name,label}", 72 | "tag_name" : "1.0.2", 73 | "name" : "1.0.2" 74 | } -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-mdiep-MDPSplitView-releases-tags-1.0.2.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/repos-mdiep-MDPSplitView-releases-tags-1.0.2.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-mdiep-NonExistent-releases-tags-tag.data: -------------------------------------------------------------------------------- 1 | { 2 | "message" : "Not Found", 3 | "documentation_url" : "https:\/\/docs.github.com\/rest\/reference\/repos#get-a-release-by-tag-name" 4 | } -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-mdiep-NonExistent-releases-tags-tag.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/repos-mdiep-NonExistent-releases-tags-tag.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-mdiep-Tentacle-contents-Carthage-Checkouts-ReactiveSwift.data: -------------------------------------------------------------------------------- 1 | { 2 | "_links" : { 3 | "git" : "https:\/\/api.github.com\/repos\/ReactiveCocoa\/ReactiveSwift\/git\/trees\/e27ccdbf4ec36f154b60b91a0d7e0110c4e882cb", 4 | "html" : "https:\/\/github.com\/ReactiveCocoa\/ReactiveSwift\/tree\/e27ccdbf4ec36f154b60b91a0d7e0110c4e882cb", 5 | "self" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/contents\/Carthage\/Checkouts\/ReactiveSwift?ref=master" 6 | }, 7 | "git_url" : "https:\/\/api.github.com\/repos\/ReactiveCocoa\/ReactiveSwift\/git\/trees\/e27ccdbf4ec36f154b60b91a0d7e0110c4e882cb", 8 | "html_url" : "https:\/\/github.com\/ReactiveCocoa\/ReactiveSwift\/tree\/e27ccdbf4ec36f154b60b91a0d7e0110c4e882cb", 9 | "download_url" : null, 10 | "size" : 0, 11 | "submodule_git_url" : "https:\/\/github.com\/ReactiveCocoa\/ReactiveSwift.git", 12 | "sha" : "e27ccdbf4ec36f154b60b91a0d7e0110c4e882cb", 13 | "path" : "Carthage\/Checkouts\/ReactiveSwift", 14 | "type" : "submodule", 15 | "name" : "ReactiveSwift", 16 | "url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/contents\/Carthage\/Checkouts\/ReactiveSwift?ref=master" 17 | } -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-mdiep-Tentacle-contents-Carthage-Checkouts-ReactiveSwift.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/repos-mdiep-Tentacle-contents-Carthage-Checkouts-ReactiveSwift.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-mdiep-Tentacle-contents-update-test-fixtures.data: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_links" : { 4 | "git" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/git\/blobs\/3b6fd366cd32ef147c27ad49353b29f1ef5daf1c", 5 | "html" : "https:\/\/github.com\/mdiep\/Tentacle\/blob\/master\/update-test-fixtures\/Info.plist", 6 | "self" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/contents\/update-test-fixtures\/Info.plist?ref=master" 7 | }, 8 | "git_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/git\/blobs\/3b6fd366cd32ef147c27ad49353b29f1ef5daf1c", 9 | "html_url" : "https:\/\/github.com\/mdiep\/Tentacle\/blob\/master\/update-test-fixtures\/Info.plist", 10 | "download_url" : "https:\/\/raw.githubusercontent.com\/mdiep\/Tentacle\/master\/update-test-fixtures\/Info.plist", 11 | "size" : 1086, 12 | "sha" : "3b6fd366cd32ef147c27ad49353b29f1ef5daf1c", 13 | "path" : "update-test-fixtures\/Info.plist", 14 | "type" : "file", 15 | "name" : "Info.plist", 16 | "url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/contents\/update-test-fixtures\/Info.plist?ref=master" 17 | }, 18 | { 19 | "_links" : { 20 | "git" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/git\/blobs\/e3fe7edcb2247a69c6eade1719d1e0cd83595cf9", 21 | "html" : "https:\/\/github.com\/mdiep\/Tentacle\/blob\/master\/update-test-fixtures\/main.swift", 22 | "self" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/contents\/update-test-fixtures\/main.swift?ref=master" 23 | }, 24 | "git_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/git\/blobs\/e3fe7edcb2247a69c6eade1719d1e0cd83595cf9", 25 | "html_url" : "https:\/\/github.com\/mdiep\/Tentacle\/blob\/master\/update-test-fixtures\/main.swift", 26 | "download_url" : "https:\/\/raw.githubusercontent.com\/mdiep\/Tentacle\/master\/update-test-fixtures\/main.swift", 27 | "size" : 2340, 28 | "sha" : "e3fe7edcb2247a69c6eade1719d1e0cd83595cf9", 29 | "path" : "update-test-fixtures\/main.swift", 30 | "type" : "file", 31 | "name" : "main.swift", 32 | "url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/contents\/update-test-fixtures\/main.swift?ref=master" 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-mdiep-Tentacle-contents-update-test-fixtures.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/repos-mdiep-Tentacle-contents-update-test-fixtures.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-mdiep-Tentacle.data: -------------------------------------------------------------------------------- 1 | { 2 | "keys_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/keys{\/key_id}", 3 | "statuses_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/statuses\/{sha}", 4 | "issues_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/issues{\/number}", 5 | "license" : { 6 | "node_id" : "MDc6TGljZW5zZTEz", 7 | "key" : "mit", 8 | "spdx_id" : "MIT", 9 | "name" : "MIT License", 10 | "url" : "https:\/\/api.github.com\/licenses\/mit" 11 | }, 12 | "issue_events_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/issues\/events{\/number}", 13 | "has_projects" : true, 14 | "id" : 53076616, 15 | "allow_forking" : true, 16 | "owner" : { 17 | "id" : 1302, 18 | "organizations_url" : "https:\/\/api.github.com\/users\/mdiep\/orgs", 19 | "received_events_url" : "https:\/\/api.github.com\/users\/mdiep\/received_events", 20 | "following_url" : "https:\/\/api.github.com\/users\/mdiep\/following{\/other_user}", 21 | "login" : "mdiep", 22 | "avatar_url" : "https:\/\/avatars.githubusercontent.com\/u\/1302?v=4", 23 | "url" : "https:\/\/api.github.com\/users\/mdiep", 24 | "node_id" : "MDQ6VXNlcjEzMDI=", 25 | "subscriptions_url" : "https:\/\/api.github.com\/users\/mdiep\/subscriptions", 26 | "repos_url" : "https:\/\/api.github.com\/users\/mdiep\/repos", 27 | "type" : "User", 28 | "html_url" : "https:\/\/github.com\/mdiep", 29 | "events_url" : "https:\/\/api.github.com\/users\/mdiep\/events{\/privacy}", 30 | "site_admin" : false, 31 | "starred_url" : "https:\/\/api.github.com\/users\/mdiep\/starred{\/owner}{\/repo}", 32 | "gists_url" : "https:\/\/api.github.com\/users\/mdiep\/gists{\/gist_id}", 33 | "gravatar_id" : "", 34 | "followers_url" : "https:\/\/api.github.com\/users\/mdiep\/followers" 35 | }, 36 | "visibility" : "public", 37 | "default_branch" : "master", 38 | "events_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/events", 39 | "subscription_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/subscription", 40 | "watchers" : 248, 41 | "network_count" : 25, 42 | "git_commits_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/git\/commits{\/sha}", 43 | "subscribers_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/subscribers", 44 | "clone_url" : "https:\/\/github.com\/mdiep\/Tentacle.git", 45 | "has_wiki" : false, 46 | "url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle", 47 | "pulls_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/pulls{\/number}", 48 | "fork" : false, 49 | "notifications_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/notifications{?since,all,participating}", 50 | "description" : "A Swift framework for the GitHub API", 51 | "collaborators_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/collaborators{\/collaborator}", 52 | "deployments_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/deployments", 53 | "archived" : false, 54 | "topics" : [ 55 | "github-api", 56 | "reactiveswift", 57 | "swift" 58 | ], 59 | "temp_clone_token" : null, 60 | "languages_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/languages", 61 | "has_issues" : true, 62 | "comments_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/comments{\/number}", 63 | "is_template" : false, 64 | "private" : false, 65 | "size" : 4336, 66 | "git_tags_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/git\/tags{\/sha}", 67 | "subscribers_count" : 8, 68 | "updated_at" : "2023-02-25T19:13:44Z", 69 | "ssh_url" : "git@github.com:mdiep\/Tentacle.git", 70 | "name" : "Tentacle", 71 | "web_commit_signoff_required" : false, 72 | "contents_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/contents\/{+path}", 73 | "archive_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/{archive_format}{\/ref}", 74 | "milestones_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/milestones{\/number}", 75 | "blobs_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/git\/blobs{\/sha}", 76 | "node_id" : "MDEwOlJlcG9zaXRvcnk1MzA3NjYxNg==", 77 | "contributors_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/contributors", 78 | "open_issues_count" : 3, 79 | "forks_count" : 25, 80 | "has_discussions" : false, 81 | "trees_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/git\/trees{\/sha}", 82 | "svn_url" : "https:\/\/github.com\/mdiep\/Tentacle", 83 | "commits_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/commits{\/sha}", 84 | "created_at" : "2016-03-03T19:20:49Z", 85 | "forks_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/forks", 86 | "has_downloads" : true, 87 | "mirror_url" : null, 88 | "homepage" : "", 89 | "teams_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/teams", 90 | "branches_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/branches{\/branch}", 91 | "disabled" : false, 92 | "issue_comment_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/issues\/comments{\/number}", 93 | "merges_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/merges", 94 | "git_refs_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/git\/refs{\/sha}", 95 | "git_url" : "git:\/\/github.com\/mdiep\/Tentacle.git", 96 | "forks" : 25, 97 | "open_issues" : 3, 98 | "hooks_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/hooks", 99 | "html_url" : "https:\/\/github.com\/mdiep\/Tentacle", 100 | "stargazers_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/stargazers", 101 | "assignees_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/assignees{\/user}", 102 | "compare_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/compare\/{base}...{head}", 103 | "full_name" : "mdiep\/Tentacle", 104 | "tags_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/tags", 105 | "releases_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/releases{\/id}", 106 | "pushed_at" : "2023-06-15T21:50:06Z", 107 | "labels_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/labels{\/name}", 108 | "downloads_url" : "https:\/\/api.github.com\/repos\/mdiep\/Tentacle\/downloads", 109 | "stargazers_count" : 248, 110 | "watchers_count" : 248, 111 | "language" : "Swift", 112 | "has_pages" : true 113 | } -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-mdiep-Tentacle.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/repos-mdiep-Tentacle.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-torvalds-linux-releases-tags-v4.4.data: -------------------------------------------------------------------------------- 1 | { 2 | "message" : "Not Found", 3 | "documentation_url" : "https:\/\/docs.github.com\/rest\/reference\/repos#get-a-release-by-tag-name" 4 | } -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/repos-torvalds-linux-releases-tags-v4.4.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/repos-torvalds-linux-releases-tags-v4.4.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/users-Palleas-Opensource-repos.data: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/keys{\/key_id}", 4 | "statuses_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/statuses\/{sha}", 5 | "issues_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/issues{\/number}", 6 | "license" : { 7 | "node_id" : "MDc6TGljZW5zZTEz", 8 | "key" : "mit", 9 | "spdx_id" : "MIT", 10 | "name" : "MIT License", 11 | "url" : "https:\/\/api.github.com\/licenses\/mit" 12 | }, 13 | "issue_events_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/issues\/events{\/number}", 14 | "has_projects" : true, 15 | "id" : 59615946, 16 | "allow_forking" : true, 17 | "owner" : { 18 | "id" : 15802020, 19 | "organizations_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/orgs", 20 | "received_events_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/received_events", 21 | "following_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/following{\/other_user}", 22 | "login" : "Palleas-opensource", 23 | "avatar_url" : "https:\/\/avatars.githubusercontent.com\/u\/15802020?v=4", 24 | "url" : "https:\/\/api.github.com\/users\/Palleas-opensource", 25 | "node_id" : "MDQ6VXNlcjE1ODAyMDIw", 26 | "subscriptions_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/subscriptions", 27 | "repos_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/repos", 28 | "type" : "User", 29 | "html_url" : "https:\/\/github.com\/Palleas-opensource", 30 | "events_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/events{\/privacy}", 31 | "site_admin" : false, 32 | "starred_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/starred{\/owner}{\/repo}", 33 | "gists_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/gists{\/gist_id}", 34 | "gravatar_id" : "", 35 | "followers_url" : "https:\/\/api.github.com\/users\/Palleas-opensource\/followers" 36 | }, 37 | "visibility" : "public", 38 | "default_branch" : "master", 39 | "events_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/events", 40 | "subscription_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/subscription", 41 | "watchers" : 0, 42 | "git_commits_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/git\/commits{\/sha}", 43 | "subscribers_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/subscribers", 44 | "clone_url" : "https:\/\/github.com\/Palleas-opensource\/Sample-repository.git", 45 | "has_wiki" : true, 46 | "url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository", 47 | "pulls_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/pulls{\/number}", 48 | "fork" : false, 49 | "notifications_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/notifications{?since,all,participating}", 50 | "description" : null, 51 | "collaborators_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/collaborators{\/collaborator}", 52 | "deployments_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/deployments", 53 | "archived" : false, 54 | "topics" : [ 55 | 56 | ], 57 | "languages_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/languages", 58 | "has_issues" : true, 59 | "comments_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/comments{\/number}", 60 | "is_template" : false, 61 | "private" : false, 62 | "size" : 7, 63 | "git_tags_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/git\/tags{\/sha}", 64 | "updated_at" : "2016-11-29T23:58:28Z", 65 | "ssh_url" : "git@github.com:Palleas-opensource\/Sample-repository.git", 66 | "name" : "Sample-repository", 67 | "web_commit_signoff_required" : false, 68 | "contents_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/contents\/{+path}", 69 | "archive_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/{archive_format}{\/ref}", 70 | "milestones_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/milestones{\/number}", 71 | "blobs_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/git\/blobs{\/sha}", 72 | "node_id" : "MDEwOlJlcG9zaXRvcnk1OTYxNTk0Ng==", 73 | "contributors_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/contributors", 74 | "open_issues_count" : 2, 75 | "forks_count" : 0, 76 | "has_discussions" : false, 77 | "trees_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/git\/trees{\/sha}", 78 | "svn_url" : "https:\/\/github.com\/Palleas-opensource\/Sample-repository", 79 | "commits_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/commits{\/sha}", 80 | "created_at" : "2016-05-24T23:38:17Z", 81 | "forks_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/forks", 82 | "has_downloads" : true, 83 | "mirror_url" : null, 84 | "homepage" : null, 85 | "teams_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/teams", 86 | "branches_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/branches{\/branch}", 87 | "disabled" : false, 88 | "issue_comment_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/issues\/comments{\/number}", 89 | "merges_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/merges", 90 | "git_refs_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/git\/refs{\/sha}", 91 | "git_url" : "git:\/\/github.com\/Palleas-opensource\/Sample-repository.git", 92 | "forks" : 0, 93 | "open_issues" : 2, 94 | "hooks_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/hooks", 95 | "html_url" : "https:\/\/github.com\/Palleas-opensource\/Sample-repository", 96 | "stargazers_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/stargazers", 97 | "assignees_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/assignees{\/user}", 98 | "compare_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/compare\/{base}...{head}", 99 | "full_name" : "Palleas-opensource\/Sample-repository", 100 | "tags_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/tags", 101 | "releases_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/releases{\/id}", 102 | "pushed_at" : "2016-12-16T19:19:50Z", 103 | "labels_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/labels{\/name}", 104 | "downloads_url" : "https:\/\/api.github.com\/repos\/Palleas-opensource\/Sample-repository\/downloads", 105 | "stargazers_count" : 0, 106 | "watchers_count" : 0, 107 | "language" : null, 108 | "has_pages" : false 109 | } 110 | ] -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/users-Palleas-Opensource-repos.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/users-Palleas-Opensource-repos.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/users-mdiep.data: -------------------------------------------------------------------------------- 1 | { 2 | "location" : "Grand Rapids, MI", 3 | "hireable" : null, 4 | "public_gists" : 4, 5 | "url" : "https:\/\/api.github.com\/users\/mdiep", 6 | "following_url" : "https:\/\/api.github.com\/users\/mdiep\/following{\/other_user}", 7 | "events_url" : "https:\/\/api.github.com\/users\/mdiep\/events{\/privacy}", 8 | "received_events_url" : "https:\/\/api.github.com\/users\/mdiep\/received_events", 9 | "company" : null, 10 | "updated_at" : "2023-02-22T15:13:57Z", 11 | "twitter_username" : null, 12 | "bio" : null, 13 | "avatar_url" : "https:\/\/avatars.githubusercontent.com\/u\/1302?v=4", 14 | "name" : "Matt Diephouse", 15 | "type" : "User", 16 | "subscriptions_url" : "https:\/\/api.github.com\/users\/mdiep\/subscriptions", 17 | "gists_url" : "https:\/\/api.github.com\/users\/mdiep\/gists{\/gist_id}", 18 | "id" : 1302, 19 | "starred_url" : "https:\/\/api.github.com\/users\/mdiep\/starred{\/owner}{\/repo}", 20 | "organizations_url" : "https:\/\/api.github.com\/users\/mdiep\/orgs", 21 | "repos_url" : "https:\/\/api.github.com\/users\/mdiep\/repos", 22 | "site_admin" : false, 23 | "email" : null, 24 | "login" : "mdiep", 25 | "blog" : "http:\/\/matt.diephouse.com", 26 | "public_repos" : 43, 27 | "followers" : 654, 28 | "following" : 8, 29 | "created_at" : "2008-02-27T23:31:47Z", 30 | "node_id" : "MDQ6VXNlcjEzMDI=", 31 | "gravatar_id" : "", 32 | "followers_url" : "https:\/\/api.github.com\/users\/mdiep\/followers", 33 | "html_url" : "https:\/\/github.com\/mdiep" 34 | } -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/users-mdiep.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/users-mdiep.response -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/users-test.data: -------------------------------------------------------------------------------- 1 | { 2 | "location" : null, 3 | "hireable" : null, 4 | "public_gists" : 0, 5 | "url" : "https:\/\/api.github.com\/users\/test", 6 | "following_url" : "https:\/\/api.github.com\/users\/test\/following{\/other_user}", 7 | "events_url" : "https:\/\/api.github.com\/users\/test\/events{\/privacy}", 8 | "received_events_url" : "https:\/\/api.github.com\/users\/test\/received_events", 9 | "company" : null, 10 | "updated_at" : "2020-04-24T20:58:44Z", 11 | "twitter_username" : null, 12 | "bio" : null, 13 | "avatar_url" : "https:\/\/avatars.githubusercontent.com\/u\/383316?v=4", 14 | "name" : null, 15 | "type" : "User", 16 | "subscriptions_url" : "https:\/\/api.github.com\/users\/test\/subscriptions", 17 | "gists_url" : "https:\/\/api.github.com\/users\/test\/gists{\/gist_id}", 18 | "id" : 383316, 19 | "starred_url" : "https:\/\/api.github.com\/users\/test\/starred{\/owner}{\/repo}", 20 | "organizations_url" : "https:\/\/api.github.com\/users\/test\/orgs", 21 | "repos_url" : "https:\/\/api.github.com\/users\/test\/repos", 22 | "site_admin" : false, 23 | "email" : null, 24 | "login" : "test", 25 | "blog" : "", 26 | "public_repos" : 5, 27 | "followers" : 48, 28 | "following" : 0, 29 | "created_at" : "2010-09-01T10:39:12Z", 30 | "node_id" : "MDQ6VXNlcjM4MzMxNg==", 31 | "gravatar_id" : "", 32 | "followers_url" : "https:\/\/api.github.com\/users\/test\/followers", 33 | "html_url" : "https:\/\/github.com\/test" 34 | } -------------------------------------------------------------------------------- /Tests/TentacleTests/Fixtures/users-test.response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdiep/Tentacle/fe0afec960dc86d9fcbc8233ea0d66b54176cffa/Tests/TentacleTests/Fixtures/users-test.response -------------------------------------------------------------------------------- /Tests/TentacleTests/GitHubErrorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitHubErrorTests.swift 3 | // Tentacle 4 | // 5 | // Created by Matt Diephouse on 3/4/16. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | @testable import Tentacle 10 | import XCTest 11 | 12 | class GitHubErrorTests: XCTestCase { 13 | func testDecode() { 14 | let expected = GitHubError(message: "Not Found") 15 | XCTAssertEqual(Fixture.Release.Nonexistent.decode(), expected) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/TentacleTests/HTTPStub.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | import Foundation 3 | 4 | // Based on https://github.com/ishkawa/APIKit/blob/3.0.0/Tests/APIKitTests/TestComponents/HTTPStub.swift. 5 | class HTTPStub: NSObject { 6 | static let shared: HTTPStub = { 7 | let s = HTTPStub() 8 | s.initialize() 9 | 10 | return s 11 | }() 12 | 13 | var stubRequests: ((URLRequest) -> FixtureType)! 14 | private var fixture: FixtureType! 15 | 16 | func initialize() { 17 | URLProtocol.registerClass(StubProtocol.self) 18 | } 19 | 20 | private override init() { 21 | super.init() 22 | } 23 | 24 | private class StubProtocol: URLProtocol { 25 | private var isCancelled = false 26 | 27 | // MARK: - URLProtocol 28 | 29 | override static func canInit(with: URLRequest) -> Bool { 30 | return true 31 | } 32 | 33 | override static func canonicalRequest(for request: URLRequest) -> URLRequest { 34 | HTTPStub.shared.fixture = HTTPStub.shared.stubRequests(request) 35 | return request 36 | } 37 | 38 | override func startLoading() { 39 | let queue = DispatchQueue.global(qos: .default) 40 | 41 | queue.asyncAfter(deadline: .now() + 0.01) { 42 | guard !self.isCancelled else { 43 | return 44 | } 45 | 46 | let fixture = HTTPStub.shared.fixture! 47 | self.client?.urlProtocol(self, didReceive: fixture.response, cacheStoragePolicy: .notAllowed) 48 | self.client?.urlProtocol(self, didLoad: fixture.data) 49 | self.client?.urlProtocolDidFinishLoading(self) 50 | } 51 | } 52 | 53 | override func stopLoading() { 54 | isCancelled = true 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/TentacleTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/TentacleTests/IssuesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IssuesTests.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2016-05-24. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | @testable import Tentacle 10 | import XCTest 11 | 12 | class IssuesTests: XCTestCase { 13 | 14 | 15 | 16 | func testDecodedPalleasOpensourceIssues() { 17 | let updateReadmePullRequest = PullRequest( 18 | url: URL(string: "https://github.com/Palleas-opensource/Sample-repository/pull/3")!, 19 | diffURL: URL(string: "https://github.com/Palleas-opensource/Sample-repository/pull/3.diff")!, 20 | patchURL: URL(string: "https://github.com/Palleas-opensource/Sample-repository/pull/3.patch")! 21 | ) 22 | 23 | let expected = [ 24 | Issue(id: 165458041, 25 | url: URL(string: "https://github.com/Palleas-opensource/Sample-repository/pull/3"), 26 | number: 3, 27 | state: .open, 28 | title: "Add informations in Readme", 29 | body: "![Giphy](http://media2.giphy.com/media/jxhJ8ylaYIPbG/giphy.gif)\n", 30 | user: .palleasOpensource, 31 | labels: [], 32 | assignees: [], 33 | milestone: nil, 34 | isLocked: false, 35 | commentCount: 0, 36 | pullRequest: updateReadmePullRequest, 37 | closedAt: nil, 38 | createdAt: DateFormatter.iso8601.date(from: "2016-07-14T01:40:08Z")!, 39 | updatedAt: DateFormatter.iso8601.date(from: "2016-07-14T01:40:08Z")!), 40 | Issue(id: 156633109, 41 | url: URL(string: "https://github.com/Palleas-opensource/Sample-repository/issues/1")!, 42 | number: 1, 43 | state: .open, 44 | title: "This issue is open", 45 | body: "Issues are pretty cool.\n", 46 | user: .palleasOpensource, 47 | labels: [ 48 | Label( 49 | name: "bug", 50 | color: Color(hex: "ee0701") 51 | ), 52 | Label( 53 | name: "duplicate", 54 | color: Color(hex: "cccccc") 55 | ), 56 | Label( 57 | name: "enhancement", 58 | color: Color(hex: "84b6eb") 59 | ) 60 | ], 61 | assignees: [.palleasOpensource], 62 | milestone: .shipIt, 63 | isLocked: false, 64 | commentCount: 2, 65 | pullRequest: nil, 66 | closedAt: nil, 67 | createdAt: DateFormatter.iso8601.date(from: "2016-05-24T23:38:39Z")!, 68 | updatedAt: DateFormatter.iso8601.date(from: "2016-07-27T01:29:31Z")! 69 | ) 70 | ] 71 | 72 | let issues: [Issue]? = Fixture.IssuesInRepository.PalleasOpensource.decode() 73 | 74 | XCTAssertEqual(issues!, expected) 75 | } 76 | 77 | func testDecodedSingleIssue() { 78 | let expected = Issue( 79 | id: 156633109, 80 | url: URL(string: "https://github.com/Palleas-opensource/Sample-repository/issues/1")!, 81 | number: 1, 82 | state: .open, 83 | title: "This issue is open", 84 | body: "Issues are pretty cool.\n", 85 | user: .palleasOpensource, 86 | labels: [ 87 | Label( 88 | name: "bug", 89 | color: Color(hex: "ee0701") 90 | ), 91 | Label( 92 | name: "duplicate", 93 | color: Color(hex: "cccccc") 94 | ), 95 | Label( 96 | name: "enhancement", 97 | color: Color(hex: "84b6eb") 98 | ) 99 | ], 100 | assignees: [.palleasOpensource], 101 | milestone: .shipIt, 102 | isLocked: false, 103 | commentCount: 2, 104 | pullRequest: nil, 105 | closedAt: nil, 106 | createdAt: DateFormatter.iso8601.date(from: "2016-05-24T23:38:39Z")!, 107 | updatedAt: DateFormatter.iso8601.date(from: "2016-07-27T01:29:31Z")! 108 | ) 109 | 110 | let issues: Issue? = Fixture.IssueInRepository.Issue1InSampleRepository.decode() 111 | 112 | XCTAssertEqual(issues!, expected) 113 | } 114 | } 115 | 116 | extension UserInfo { 117 | static let palleasOpensource = UserInfo( 118 | id: 15802020, 119 | user: User("Palleas-opensource"), 120 | url: URL(string: "https://github.com/Palleas-opensource")!, 121 | avatarURL: URL(string: "https://avatars.githubusercontent.com/u/15802020?v=3")!, 122 | type: .user 123 | ) 124 | } 125 | 126 | extension Milestone { 127 | static let shipIt = Milestone( 128 | id: 1881390, 129 | number: 1, 130 | state: .open, 131 | title: "Release this app", 132 | body: "That'd be cool", 133 | creator: .palleasOpensource, 134 | openIssueCount: 1, 135 | closedIssueCount: 0, 136 | createdAt: DateFormatter.iso8601.date(from: "2016-07-13T16:56:48Z")!, 137 | updatedAt: DateFormatter.iso8601.date(from: "2016-07-13T16:56:57Z")!, 138 | closedAt: nil, 139 | dueOn: DateFormatter.iso8601.date(from: "2016-07-25T04:00:00Z")!, 140 | url: URL(string: "https://api.github.com/repos/Palleas-opensource/Sample-repository/milestones/1")! 141 | ) 142 | 143 | } 144 | -------------------------------------------------------------------------------- /Tests/TentacleTests/ReleaseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReleaseTests.swift 3 | // Tentacle 4 | // 5 | // Created by Matt Diephouse on 3/3/16. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | @testable import Tentacle 10 | import XCTest 11 | 12 | class ReleaseTests: XCTestCase { 13 | func testDecode() { 14 | let expected = Release( 15 | id: 2698201, 16 | tag: "0.15", 17 | url: URL(string: "https://github.com/Carthage/Carthage/releases/tag/0.15")!, 18 | name: "0.15: YOLOL", 19 | assets: [ 20 | Release.Asset( 21 | id: 1358331, 22 | name: "Carthage.pkg", 23 | contentType: "application/octet-stream", 24 | url: URL(string: "https://github.com/Carthage/Carthage/releases/download/0.15/Carthage.pkg")!, 25 | apiURL: URL(string: "https://api.github.com/repos/Carthage/Carthage/releases/assets/1358331")! 26 | ), 27 | Release.Asset( 28 | id: 1358332, 29 | name: "CarthageKit.framework.zip", 30 | contentType: "application/zip", 31 | url: URL(string: "https://github.com/Carthage/Carthage/releases/download/0.15/CarthageKit.framework.zip")!, 32 | apiURL: URL(string: "https://api.github.com/repos/Carthage/Carthage/releases/assets/1358332")! 33 | ) 34 | ] 35 | ) 36 | XCTAssertEqual(Fixture.Release.Carthage0_15.decode(), expected) 37 | } 38 | 39 | func testDecodeLatestRelease() { 40 | let expected = Release( 41 | id: 961251, 42 | tag: "1.0.2", 43 | url: URL(string: "https://github.com/mdiep/MDPSplitView/releases/tag/1.0.2")!, 44 | name: "1.0.2", 45 | assets: [ 46 | Release.Asset( 47 | id: 433845, 48 | name: "MDPSplitView.framework.zip", 49 | contentType: "application/zip", 50 | url: URL(string: "https://github.com/mdiep/MDPSplitView/releases/download/1.0.2/MDPSplitView.framework.zip")!, 51 | apiURL: URL(string: "https://api.github.com/repos/mdiep/MDPSplitView/releases/assets/433845")! 52 | ), 53 | ] 54 | ) 55 | XCTAssertEqual(Fixture.LatestRelease.release.decode(), expected) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/TentacleTests/RepositoryInfoTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepositoryInfoTests.swift 3 | // Tentacle 4 | // 5 | // Created by Romain Pouclet on 2016-08-02. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tentacle 11 | 12 | class RepositoryInfoTests: XCTestCase { 13 | 14 | func testUserRepositoryInfoAreEquals() { 15 | let palleasOpensource = UserInfo( 16 | id: 15802020, 17 | user: User("Palleas-opensource"), 18 | url: URL(string: "https://github.com/Palleas-opensource")!, 19 | avatarURL: URL(string: "https://avatars.githubusercontent.com/u/15802020?v=3")!, 20 | type: .user 21 | ) 22 | 23 | let expected = [ 24 | RepositoryInfo( 25 | id: 59615946, 26 | owner: palleasOpensource, 27 | name: "Sample-repository", 28 | nameWithOwner: "Palleas-opensource/Sample-repository", 29 | body: "", 30 | url: URL(string: "https://github.com/Palleas-opensource/Sample-repository")!, 31 | homepage: nil, 32 | isPrivate: false, 33 | isFork: false, 34 | forksCount: 0, 35 | stargazersCount: 0, 36 | watchersCount: 0, 37 | openIssuesCount: 2, 38 | pushedAt: DateFormatter.iso8601.date(from: "2016-07-14T01:40:08Z")!, 39 | createdAt: DateFormatter.iso8601.date(from: "2016-05-24T23:38:17Z")!, 40 | updatedAt: DateFormatter.iso8601.date(from: "2016-05-24T23:38:17Z")! 41 | ) 42 | ] 43 | 44 | let decoded: [RepositoryInfo] = Fixture.RepositoriesForUser.RepositoriesForPalleasOpensource.decode()! 45 | 46 | XCTAssertEqual(decoded, expected) 47 | } 48 | 49 | func testOrganizationRepositoryAreEqual() { 50 | let raccommunity = UserInfo( 51 | id: 18710012, 52 | user: User("RACCommunity"), 53 | url: URL(string: "https://github.com/RACCommunity")!, 54 | avatarURL: URL(string: "https://avatars.githubusercontent.com/u/18710012?v=3")!, 55 | type: .organization 56 | ) 57 | 58 | let expected = [ 59 | RepositoryInfo( 60 | id: 35350514, 61 | owner: raccommunity, 62 | name: "Rex", 63 | nameWithOwner: "RACCommunity/Rex", 64 | body: "ReactiveCocoa Extensions", 65 | url: URL(string: "https://github.com/RACCommunity")!, 66 | homepage: nil, 67 | isPrivate: false, 68 | isFork: false, 69 | forksCount: 36, 70 | stargazersCount: 227, 71 | watchersCount: 227, 72 | openIssuesCount: 13, 73 | pushedAt: DateFormatter.iso8601.date(from: "2017-07-01T17:16:29Z")!, 74 | createdAt: DateFormatter.iso8601.date(from: "2015-05-10T00:15:08Z")!, 75 | updatedAt: DateFormatter.iso8601.date(from: "2023-05-24T18:46:44Z")! 76 | ), 77 | RepositoryInfo( 78 | id: 49464897, 79 | owner: raccommunity, 80 | name: "RACNest", 81 | nameWithOwner: "RACCommunity/RACNest", 82 | body: "RAC + MVVM examples :mouse::mouse::mouse:", 83 | url: URL(string: "https://github.com/RACCommunity/RACNest")!, 84 | homepage: nil, 85 | isPrivate: false, 86 | isFork: false, 87 | forksCount: 16, 88 | stargazersCount: 139, 89 | watchersCount: 139, 90 | openIssuesCount: 3, 91 | pushedAt: DateFormatter.iso8601.date(from: "2019-05-07T14:53:43Z")!, 92 | createdAt: DateFormatter.iso8601.date(from: "2016-01-12T01:00:02Z")!, 93 | updatedAt: DateFormatter.iso8601.date(from: "2022-02-14T02:43:41Z")! 94 | ), 95 | RepositoryInfo( 96 | id: 57858100, 97 | owner: raccommunity, 98 | name: "contributors", 99 | nameWithOwner: "RACCommunity/contributors", 100 | body: "ReactiveCocoa's Community Guidelines", 101 | url: URL(string: "https://github.com/RACCommunity")!, 102 | homepage: nil, 103 | isPrivate: false, 104 | isFork: false, 105 | forksCount: 1, 106 | stargazersCount: 16, 107 | watchersCount: 16, 108 | openIssuesCount: 3, 109 | pushedAt: DateFormatter.iso8601.date(from: "2016-05-02T10:35:31Z")!, 110 | createdAt: DateFormatter.iso8601.date(from: "2016-05-02T00:27:44Z")!, 111 | updatedAt: DateFormatter.iso8601.date(from: "2016-07-27T11:39:23Z")! 112 | ), 113 | RepositoryInfo( 114 | id: 59124784, 115 | owner: raccommunity, 116 | name: "racurated", 117 | nameWithOwner: "RACCommunity/racurated", 118 | body: "Curated list of ReactiveCocoa projects.", 119 | url: URL(string: "https://github.com/RACCommunity/racurated")!, 120 | homepage: URL(string: "https://raccommunity.github.io/racurated/"), 121 | isPrivate: false, 122 | isFork: false, 123 | forksCount: 0, 124 | stargazersCount: 11, 125 | watchersCount: 11, 126 | openIssuesCount: 0, 127 | pushedAt: DateFormatter.iso8601.date(from: "2017-09-30T21:00:08Z")!, 128 | createdAt: DateFormatter.iso8601.date(from: "2016-05-18T14:47:59Z")!, 129 | updatedAt: DateFormatter.iso8601.date(from: "2019-02-08T17:26:04Z")! 130 | ), 131 | RepositoryInfo( 132 | id: 75979247, 133 | owner: raccommunity, 134 | name: "ReactiveCollections", 135 | nameWithOwner: "RACCommunity/ReactiveCollections", 136 | body: "Reactive collections for Swift using ReactiveSwift 🚗 🚕 🚙 ", 137 | url: URL(string: "https://github.com/RACCommunity/ReactiveCollections")!, 138 | homepage: nil, 139 | isPrivate: false, 140 | isFork: false, 141 | forksCount: 5, 142 | stargazersCount: 40, 143 | watchersCount: 40, 144 | openIssuesCount: 1, 145 | pushedAt: DateFormatter.iso8601.date(from: "2018-04-20T20:51:14Z")!, 146 | createdAt: DateFormatter.iso8601.date(from: "2016-12-08T22:08:36Z")!, 147 | updatedAt: DateFormatter.iso8601.date(from: "2023-01-28T14:24:30Z")!), 148 | RepositoryInfo( 149 | id: 88407587, 150 | owner: raccommunity, 151 | name: "jazzy", 152 | nameWithOwner: "RACCommunity/jazzy", 153 | body: "Soulful docs for Swift & Objective-C", 154 | url: URL(string: "https://github.com/RACCommunity/jazzy")!, 155 | homepage: URL(string: "https://realm.io"), 156 | isPrivate: false, 157 | isFork: true, 158 | forksCount: 0, 159 | stargazersCount: 0, 160 | watchersCount: 0, 161 | openIssuesCount: 1, 162 | pushedAt: DateFormatter.iso8601.date(from: "2017-04-16T14:44:51Z")!, 163 | createdAt: DateFormatter.iso8601.date(from: "2017-04-16T11:00:24Z")!, 164 | updatedAt: DateFormatter.iso8601.date(from: "2017-04-16T11:00:26Z")!), 165 | RepositoryInfo( 166 | id: 92775322, 167 | owner: raccommunity, 168 | name: "ReactiveRxBridge", 169 | nameWithOwner: "RACCommunity/ReactiveRxBridge", 170 | body: nil, 171 | url: URL(string: "https://github.com/RACCommunity/ReactiveRxBridge")!, 172 | homepage: nil, 173 | isPrivate: false, 174 | isFork: false, 175 | forksCount: 0, 176 | stargazersCount: 2, 177 | watchersCount: 2, 178 | openIssuesCount: 0, 179 | pushedAt: DateFormatter.iso8601.date(from: "2017-05-29T21:25:15Z")!, 180 | createdAt: DateFormatter.iso8601.date(from: "2017-05-29T21:04:52Z")!, 181 | updatedAt: DateFormatter.iso8601.date(from: "2020-03-10T21:45:19Z")!), 182 | RepositoryInfo( 183 | id: 107228565, 184 | owner: raccommunity, 185 | name: "FlexibleDiff", 186 | nameWithOwner: "RACCommunity/FlexibleDiff", 187 | body: "A Swift collection diffing μframework.", 188 | url: URL(string: "https://github.com/RACCommunity/FlexibleDiff")!, 189 | homepage: nil, 190 | isPrivate: false, 191 | isFork: false, 192 | forksCount: 9, 193 | stargazersCount: 105, 194 | watchersCount: 105, 195 | openIssuesCount: 2, 196 | pushedAt: DateFormatter.iso8601.date(from: "2021-03-08T18:09:25Z")!, 197 | createdAt: DateFormatter.iso8601.date(from: "2017-10-17T06:45:43Z")!, 198 | updatedAt: DateFormatter.iso8601.date(from: "2022-10-13T04:09:12Z")!) 199 | ] 200 | 201 | let decoded: [RepositoryInfo] = Fixture.RepositoriesForOrganization.RepositoriesForRACCommunity.decode()! 202 | 203 | XCTAssertEqual(decoded, expected) 204 | } 205 | 206 | func testDecodedRepositoryInfo() { 207 | let mdiep = UserInfo( 208 | id: 18710012, 209 | user: User("mdiep"), 210 | url: URL(string: "https://github.com/mdiep")!, 211 | avatarURL: URL(string: "https://avatars2.githubusercontent.com/u/1302?v=4")!, 212 | type: .user 213 | ) 214 | 215 | let expected = RepositoryInfo( 216 | id: 53076616, 217 | owner: mdiep, 218 | name: "Tentacle", 219 | nameWithOwner: "mdiep/Tentacle", 220 | body: "A Swift framework for the GitHub API", 221 | url: URL(string: "https://github.com/mdiep/Tentacle")!, 222 | homepage: nil, 223 | isPrivate: false, 224 | isFork: false, 225 | forksCount: 16, 226 | stargazersCount: 189, 227 | watchersCount: 189, 228 | openIssuesCount: 1, 229 | pushedAt: DateFormatter.iso8601.date(from: "2017-11-25T06:36:01Z")!, 230 | createdAt: DateFormatter.iso8601.date(from: "2016-03-03T19:20:49Z")!, 231 | updatedAt: DateFormatter.iso8601.date(from: "2017-11-26T16:01:50Z")! 232 | ) 233 | 234 | let decoded: RepositoryInfo = Fixture.Repositories.Tentacle.decode()! 235 | 236 | XCTAssertEqual(decoded, expected) 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /Tests/TentacleTests/RepositoryTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepositoryTests.swift 3 | // Tentacle 4 | // 5 | // Created by Matt Diephouse on 3/20/16. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Tentacle 10 | import XCTest 11 | 12 | class RepositoryTests: XCTestCase { 13 | func testEquality() { 14 | let repo1 = Repository(owner: "mdiep", name: "Tentacle") 15 | let repo2 = Repository(owner: "mdiep", name: "TENTACLE") 16 | let repo3 = Repository(owner: "MDIEP", name: "Tentacle") 17 | XCTAssertEqual(repo1, repo2) 18 | XCTAssertEqual(repo1, repo3) 19 | XCTAssertEqual(repo1.hashValue, repo2.hashValue) 20 | XCTAssertEqual(repo1.hashValue, repo3.hashValue) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/TentacleTests/ResponseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResponseTests.swift 3 | // Tentacle 4 | // 5 | // Created by Matt Diephouse on 3/17/16. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | @testable import Tentacle 10 | import XCTest 11 | 12 | class ResponseTests: XCTestCase { 13 | func testInitWithHeaderFields() { 14 | let headers = [ 15 | "X-RateLimit-Remaining": "4987", 16 | "X-RateLimit-Reset": "1350085394", 17 | "Link": "; rel=\"next\", ; rel=\"last\"" 18 | ] 19 | 20 | let response = Response(headerFields: headers) 21 | XCTAssertEqual(response.rateLimitRemaining, 4987) 22 | XCTAssertEqual(response.rateLimitReset, Date(timeIntervalSince1970: 1350085394)) 23 | XCTAssertEqual( 24 | response.links, 25 | [ 26 | "next": URL(string: "https://api.github.com/user/repos?page=3&per_page=100")!, 27 | "last": URL(string: "https://api.github.com/user/repos?page=50&per_page=100")!, 28 | ] 29 | ) 30 | } 31 | 32 | // Enterprise Instances don't have rate-limit fields. 33 | func testInitWithNoRateLimitFields() { 34 | let headers = [ 35 | "Link": "; rel=\"next\", ; rel=\"last\"" 36 | ] 37 | 38 | let response = Response(headerFields: headers) 39 | XCTAssertNil(response.rateLimitRemaining) 40 | XCTAssertNil(response.rateLimitReset) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Tests/TentacleTests/ServerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServerTests.swift 3 | // Tentacle 4 | // 5 | // Created by Matt Diephouse on 3/20/16. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | @testable import Tentacle 10 | import XCTest 11 | 12 | class ServerTests: XCTestCase { 13 | func testEquality() { 14 | let server1 = Server.enterprise(url: URL(string: "https://example.com")!) 15 | let server2 = Server.enterprise(url: URL(string: "https://EXAMPLE.COM")!) 16 | XCTAssertEqual(server1, server2) 17 | XCTAssertEqual(server1.hashValue, server2.hashValue) 18 | } 19 | 20 | func testEndpoint() { 21 | let dotCom = Server.dotCom 22 | XCTAssertEqual(dotCom.endpoint, "https://api.github.com") 23 | 24 | let enterprise = Server.enterprise(url: URL(string: "https://example.com")!) 25 | XCTAssertEqual(enterprise.endpoint, "https://example.com/api/v3") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/TentacleTests/TreeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TreeTests.swift 3 | // Tentacle 4 | // 5 | // Created by David Caunt on 21/04/2017. 6 | // Copyright © 2017 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | @testable import Tentacle 12 | 13 | class TreeTests: XCTestCase { 14 | 15 | let entries = [ 16 | Tree.Entry( 17 | type: .tree( 18 | url: URL(string: "https://api.github.com/repos/Palleas-opensource/Sample-repository/git/trees/5bfad2b3f8e483b6b173d8aaff19597e84626f15")! 19 | ), 20 | sha: "5bfad2b3f8e483b6b173d8aaff19597e84626f15", 21 | path: "Directory", 22 | mode: .subdirectory 23 | ), 24 | Tree.Entry( 25 | type: .blob( 26 | url: URL(string: "https://api.github.com/repos/Palleas-opensource/Sample-repository/git/blobs/c3eb8708a0a5aaa4f685aab24ef6403fbfd28efc")!, 27 | size: 18 28 | ), 29 | sha: "c3eb8708a0a5aaa4f685aab24ef6403fbfd28efc", 30 | path: "README.markdown", 31 | mode: .file 32 | ), 33 | Tree.Entry( 34 | type: .commit, 35 | sha: "7a84505a3c553fd8e2879cfa63753b0cd212feb8", 36 | path: "Tentacle", 37 | mode: .submodule 38 | ), 39 | Tree.Entry( 40 | type: .blob( 41 | url: URL(string: "https://api.github.com/repos/Palleas-opensource/Sample-repository/git/blobs/1e3f1fd0bc1f65cf4701c217f4d1fd9a3cd50721")!, 42 | size: 12 43 | ), 44 | sha: "1e3f1fd0bc1f65cf4701c217f4d1fd9a3cd50721", 45 | path: "say", 46 | mode: .file 47 | ) 48 | ] 49 | 50 | func testDecodingTrees() { 51 | let expected = Tree( 52 | sha: "0c0dfafa361836e11aedcbb95c1f05d3f654aef0", 53 | url: URL(string: "https://api.github.com/repos/Palleas-opensource/Sample-repository/git/trees/0c0dfafa361836e11aedcbb95c1f05d3f654aef0")!, 54 | entries: entries, 55 | isTruncated: false 56 | ) 57 | 58 | XCTAssertEqual(Fixture.TreeForRepository.TreeInSampleRepository.decode()!, expected) 59 | } 60 | 61 | func testTreeEncoding() throws { 62 | let newTree = NewTree(entries: entries, base: "5bfad2b3f8e483b6b173d8aaff19597e84626f15") 63 | 64 | let encoder = JSONEncoder() 65 | let encodedTree = try encoder.encode(newTree) 66 | 67 | let decoder = JSONDecoder() 68 | let decodedTree = try decoder.decode(NewTree.self, from: encodedTree) 69 | 70 | let expected = NewTree(entries: entries, base: "5bfad2b3f8e483b6b173d8aaff19597e84626f15") 71 | XCTAssertEqual(decodedTree, expected) 72 | } 73 | 74 | func testTreeEncodingWithoutBase() { 75 | let newTree = NewTree(entries: [], base: nil) 76 | 77 | let encoder = JSONEncoder() 78 | let encodedTree = try! encoder.encode(newTree) 79 | 80 | let decoder = JSONDecoder() 81 | let decodedTree = try! decoder.decode(NewTree.self, from: encodedTree) 82 | 83 | let expected = NewTree(entries: [], base: nil) 84 | XCTAssertEqual(decodedTree, expected) 85 | } 86 | } 87 | 88 | extension NewTree: Equatable { 89 | public static func ==(lhs: NewTree, rhs: NewTree) -> Bool { 90 | return lhs.base == rhs.base && lhs.entries == rhs.entries 91 | } 92 | } 93 | 94 | extension NewTree: Decodable { 95 | public init(from decoder: Decoder) throws { 96 | let container = try decoder.container(keyedBy: NewTree.CodingKeys.self) 97 | let entries = try container.decode([Tree.Entry].self, forKey: .entries) 98 | let base = try container.decodeIfPresent(String.self, forKey: .base) 99 | self.init(entries: entries, base: base) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Tests/TentacleTests/UserTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserTests.swift 3 | // Tentacle 4 | // 5 | // Created by Matt Diephouse on 4/12/16. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | @testable import Tentacle 10 | import XCTest 11 | 12 | class UserTests: XCTestCase { 13 | func testDecodeMdiep() { 14 | let expected = UserProfile( 15 | user: UserInfo( 16 | id: 1302, 17 | user: User("mdiep"), 18 | url: URL(string: "https://github.com/mdiep")!, 19 | avatarURL: URL(string: "https://avatars.githubusercontent.com/u/1302?v=4")!, 20 | type: .user 21 | ), 22 | joinedDate: Date(timeIntervalSince1970: 1204155107), 23 | name: "Matt Diephouse", 24 | email: nil, 25 | websiteURL: "http://matt.diephouse.com", 26 | company: nil 27 | ) 28 | XCTAssertEqual(Fixture.UserProfile.mdiep.decode(), expected) 29 | } 30 | 31 | func testDecodeTest() { 32 | let expected = UserProfile( 33 | user: UserInfo( 34 | id: 383316, 35 | user: User("test"), 36 | url: URL(string: "https://github.com/test")!, 37 | avatarURL: URL(string: "https://avatars.githubusercontent.com/u/383316?v=4")!, 38 | type: .user 39 | ), 40 | joinedDate: Date(timeIntervalSince1970: 1283337552), 41 | name: nil, 42 | email: nil, 43 | websiteURL: "", 44 | company: nil 45 | ) 46 | XCTAssertEqual(Fixture.UserProfile.test.decode(), expected) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /script/cibuild: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use strict; 4 | 5 | my ($WORKSPACE, $SCHEME, $ACTION) = @ARGV; 6 | 7 | my $buildSettings = qx{xcodebuild -workspace "$WORKSPACE" -scheme "$SCHEME" -showBuildSettings}; 8 | 9 | my @args = ("-workspace", $WORKSPACE, "-scheme", $SCHEME, "-configuration", "Release", split(/\s+/, $ACTION), "CODE_SIGNING_REQUIRED=NO", "CODE_SIGN_IDENTITY=", "ENABLE_TESTABILITY=YES"); 10 | 11 | if ($buildSettings =~ /\bPLATFORM_NAME = iphoneos/i) { 12 | unshift @args, "-destination", "name=iPhone 14"; 13 | unshift @args, "-sdk", "iphonesimulator"; 14 | } 15 | 16 | print "xcodebuild @args\n"; 17 | exec("xcodebuild", @args); 18 | -------------------------------------------------------------------------------- /update-test-fixtures/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | Copyright © 2016 Matt Diephouse. All rights reserved. 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /update-test-fixtures/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // Tentacle 4 | // 5 | // Created by Matt Diephouse on 3/3/16. 6 | // Copyright © 2016 Matt Diephouse. All rights reserved. 7 | // 8 | 9 | // A "script" to automatically download the test fixtures needed for Tentacle's unit tests. 10 | // This makes it easy to keep the fixtures up-to-date. 11 | 12 | import Foundation 13 | import ReactiveSwift 14 | @testable import Tentacle 15 | 16 | let baseURL = URL(fileURLWithPath: CommandLine.arguments[1]) 17 | 18 | let fileManager = FileManager.default 19 | let client = Client(.dotCom) 20 | let session = URLSession.shared 21 | let result = SignalProducer(Fixture.allFixtures) 22 | .flatMap(.concat) { fixture -> SignalProducer<(), Error> in 23 | let request = client.urlRequest(for: fixture.url, contentType: fixture.contentType) 24 | let dataURL = baseURL.appendingPathComponent(fixture.dataFilename) 25 | let responseURL = baseURL.appendingPathComponent(fixture.responseFilename) 26 | let path = (dataURL.path as NSString).abbreviatingWithTildeInPath 27 | print("*** Downloading \(request.url!)\n to \(path)") 28 | return session 29 | .reactive 30 | .data(with: request) 31 | .on(failed: { error in 32 | print("***** Download failed: \(error)") 33 | }) 34 | .on(value: { data, response in 35 | 36 | let existing = try? Data(contentsOf: dataURL) 37 | let changed = existing != data 38 | 39 | if changed { 40 | try! fileManager.createDirectory(at: dataURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil) 41 | 42 | let JSONResponse = try! JSONSerialization.jsonObject(with: data, options: .allowFragments) 43 | let formattedData = try! JSONSerialization.data(withJSONObject: JSONResponse, options: .prettyPrinted) 44 | try? formattedData.write(to: dataURL, options: .atomic) 45 | } 46 | 47 | if changed || !fileManager.fileExists(atPath: responseURL.path) { 48 | try? NSKeyedArchiver 49 | .archivedData(withRootObject: response, requiringSecureCoding: false) 50 | .write(to: responseURL, options: .atomic) 51 | } 52 | }) 53 | .map { _, _ in () } 54 | } 55 | .wait() 56 | 57 | switch result { 58 | case .success: 59 | print("Successfully updated text fixtures") 60 | case let .failure(error): 61 | print("Error updating fixtures: \(error)") 62 | } 63 | --------------------------------------------------------------------------------