Skip to content

Commit cc8251b

Browse files
authored
Add Git Trees and Refs API endpoints (#40) (#211)
* Add Git Trees and Refs API endpoints (#40) - GitTree, GitTreeEntry, GitRef, GitObject models in Git.swift - tree() and listRefs() with callback and async/await variants - recursive flag maps to ?recursive=1 query param - CLI git get-tree and get-refs subcommands - Fixtures and 8 tests covering sync/async paths * Regenerate fixtures from live API and fix tests - Replace hand-crafted fixtures with real GitHub API responses via CLI - git_tree.json: real tree for nerdishbynature/octokit.swift main branch - git_refs.json: real refs listing for nerdishbynature/octokit.swift - Remove unused git_ref.json fixture - Fix GetRefs CLI: ref arg changed to --ref option to avoid path ambiguity - Update test assertions to match real fixture data * Run swiftformat and add git fixtures to update-fixtures.sh - swiftformat applied to GitTests.swift - update-fixtures.sh: add git get-refs and git get-tree commands - tree SHA resolved dynamically at runtime via GitHub API * Use fixed tree SHA in update-fixtures.sh
1 parent 0de3667 commit cc8251b

7 files changed

Lines changed: 753 additions & 9 deletions

File tree

OctoKit/Git.swift

Lines changed: 114 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,105 @@ import RequestKit
1212
import FoundationNetworking
1313
#endif
1414

15-
// MARK: request
15+
// MARK: - Models
16+
17+
public class GitTree: Codable {
18+
public var sha: String
19+
public var url: String
20+
public var tree: [GitTreeEntry]
21+
public var isTruncated: Bool
22+
23+
enum CodingKeys: String, CodingKey {
24+
case sha, url, tree
25+
case isTruncated = "truncated"
26+
}
27+
}
28+
29+
public class GitTreeEntry: Codable {
30+
public var path: String?
31+
public var mode: String?
32+
public var type: String?
33+
public var size: Int?
34+
public var sha: String?
35+
public var url: String?
36+
}
37+
38+
public class GitRef: Codable {
39+
public var ref: String
40+
public var nodeId: String
41+
public var url: String
42+
public var object: GitObject
43+
44+
enum CodingKeys: String, CodingKey {
45+
case ref, url, object
46+
case nodeId = "node_id"
47+
}
48+
}
49+
50+
public class GitObject: Codable {
51+
public var type: String
52+
public var sha: String
53+
public var url: String
54+
}
55+
56+
// MARK: - Requests
1657

1758
public extension Octokit {
59+
/// Fetches a git tree.
60+
@discardableResult
61+
func tree(owner: String,
62+
repository: String,
63+
treeSHA: String,
64+
recursive: Bool = false,
65+
completion: @escaping (_ response: Result<GitTree, Error>) -> Void) -> URLSessionDataTaskProtocol? {
66+
let router = GITRouter.tree(configuration, owner, repository, treeSHA, recursive)
67+
return router.load(session, decoder: configuration.decoder, expectedResultType: GitTree.self) { tree, error in
68+
if let error = error {
69+
completion(.failure(error))
70+
} else if let tree = tree {
71+
completion(.success(tree))
72+
}
73+
}
74+
}
75+
76+
#if compiler(>=5.5.2) && canImport(_Concurrency)
77+
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
78+
func tree(owner: String,
79+
repository: String,
80+
treeSHA: String,
81+
recursive: Bool = false) async throws -> GitTree {
82+
let router = GITRouter.tree(configuration, owner, repository, treeSHA, recursive)
83+
return try await router.load(session, decoder: configuration.decoder, expectedResultType: GitTree.self)
84+
}
85+
#endif
86+
87+
/// Lists git refs for a repository, optionally filtered by a ref prefix.
88+
@discardableResult
89+
func listRefs(owner: String,
90+
repository: String,
91+
ref: String? = nil,
92+
completion: @escaping (_ response: Result<[GitRef], Error>) -> Void) -> URLSessionDataTaskProtocol? {
93+
let router = GITRouter.listRefs(configuration, owner, repository, ref)
94+
return router.load(session, decoder: configuration.decoder, expectedResultType: [GitRef].self) { refs, error in
95+
if let error = error {
96+
completion(.failure(error))
97+
} else if let refs = refs {
98+
completion(.success(refs))
99+
}
100+
}
101+
}
102+
103+
#if compiler(>=5.5.2) && canImport(_Concurrency)
104+
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
105+
func listRefs(owner: String,
106+
repository: String,
107+
ref: String? = nil) async throws -> [GitRef] {
108+
let router = GITRouter.listRefs(configuration, owner, repository, ref)
109+
return try await router.load(session, decoder: configuration.decoder, expectedResultType: [GitRef].self)
110+
}
111+
#endif
112+
18113
/// Deletes a reference.
19-
/// - Parameters:
20-
/// - owner: The user or organization that owns the repositories.
21-
/// - repo: The repository on which the reference needs to be deleted.
22-
/// - ref: The reference to delete.
23-
/// - completion: Callback for the outcome of the deletion.
24114
@discardableResult
25115
func deleteReference(owner: String,
26116
repository: String,
@@ -31,40 +121,55 @@ public extension Octokit {
31121
}
32122
}
33123

34-
// MARK: Router
124+
// MARK: - Router
35125

36126
enum GITRouter: JSONPostRouter {
127+
case tree(Configuration, String, String, String, Bool)
128+
case listRefs(Configuration, String, String, String?)
37129
case deleteReference(Configuration, String, String, String)
38130

39131
var configuration: Configuration {
40132
switch self {
133+
case let .tree(config, _, _, _, _): return config
134+
case let .listRefs(config, _, _, _): return config
41135
case let .deleteReference(config, _, _, _): return config
42136
}
43137
}
44138

45139
var method: HTTPMethod {
46140
switch self {
141+
case .tree, .listRefs:
142+
return .GET
47143
case .deleteReference:
48144
return .DELETE
49145
}
50146
}
51147

52148
var encoding: HTTPEncoding {
53149
switch self {
54-
case .deleteReference:
150+
case .tree, .listRefs, .deleteReference:
55151
return .url
56152
}
57153
}
58154

59155
var params: [String: Any] {
60156
switch self {
61-
case .deleteReference:
157+
case let .tree(_, _, _, _, recursive):
158+
return recursive ? ["recursive": "1"] : [:]
159+
case .listRefs, .deleteReference:
62160
return [:]
63161
}
64162
}
65163

66164
var path: String {
67165
switch self {
166+
case let .tree(_, owner, repo, sha, _):
167+
return "repos/\(owner)/\(repo)/git/trees/\(sha)"
168+
case let .listRefs(_, owner, repo, ref):
169+
if let ref = ref {
170+
return "repos/\(owner)/\(repo)/git/refs/\(ref)"
171+
}
172+
return "repos/\(owner)/\(repo)/git/refs"
68173
case let .deleteReference(_, owner, repo, reference):
69174
return "repos/\(owner)/\(repo)/git/refs/\(reference)"
70175
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import ArgumentParser
2+
import Foundation
3+
import OctoKit
4+
import Rainbow
5+
6+
@available(macOS 12.0, *)
7+
struct Git: AsyncParsableCommand {
8+
static let configuration = CommandConfiguration(abstract: "Operate on Git objects",
9+
subcommands: [
10+
GetTree.self,
11+
GetRefs.self
12+
])
13+
14+
init() {}
15+
}
16+
17+
@available(macOS 12.0, *)
18+
extension Git {
19+
struct GetTree: AsyncParsableCommand {
20+
@Argument(help: "The owner of the repository")
21+
var owner: String
22+
23+
@Argument(help: "The name of the repository")
24+
var repository: String
25+
26+
@Argument(help: "The tree SHA")
27+
var treeSHA: String
28+
29+
@Flag(help: "Fetch tree recursively")
30+
var recursive: Bool = false
31+
32+
@Argument(help: "The path to put the file in")
33+
var filePath: String?
34+
35+
@Flag(help: "Verbose output flag")
36+
var verbose: Bool = false
37+
38+
init() {}
39+
40+
mutating func run() async throws {
41+
let session = JSONInterceptingURLSession()
42+
let octokit = makeOctokit(session: session)
43+
_ = try await octokit.tree(owner: owner, repository: repository, treeSHA: treeSHA, recursive: recursive)
44+
session.verbosePrint(verbose: verbose)
45+
try session.printResponseToFileOrConsole(filePath: filePath)
46+
}
47+
}
48+
49+
struct GetRefs: AsyncParsableCommand {
50+
@Argument(help: "The owner of the repository")
51+
var owner: String
52+
53+
@Argument(help: "The name of the repository")
54+
var repository: String
55+
56+
@Option(help: "Optional ref prefix to filter (e.g. heads, tags)")
57+
var ref: String?
58+
59+
@Argument(help: "The path to put the file in")
60+
var filePath: String?
61+
62+
@Flag(help: "Verbose output flag")
63+
var verbose: Bool = false
64+
65+
init() {}
66+
67+
mutating func run() async throws {
68+
let session = JSONInterceptingURLSession()
69+
let octokit = makeOctokit(session: session)
70+
_ = try await octokit.listRefs(owner: owner, repository: repository, ref: ref)
71+
session.verbosePrint(verbose: verbose)
72+
try session.printResponseToFileOrConsole(filePath: filePath)
73+
}
74+
}
75+
}

OctoKitCLI/Sources/OctoKitCLI/OctoKitCLI.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public struct OctokitCLI: AsyncParsableCommand {
1919
Status.self,
2020
User.self,
2121
Gist.self,
22+
Git.self,
2223
Notification.self,
2324
SortedJSONKeys.self
2425
])

0 commit comments

Comments
 (0)