forked from Carthage/Carthage
-
Notifications
You must be signed in to change notification settings - Fork 0
/
GitHub.swift
189 lines (160 loc) · 6.61 KB
/
GitHub.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
import Foundation
import Result
import ReactiveSwift
import Tentacle
/// The User-Agent to use for GitHub requests.
private func gitHubUserAgent() -> String {
let identifier = Constants.bundleIdentifier
let version = CarthageKitVersion.current.value
return "\(identifier)/\(version)"
}
extension Server {
/// The URL that should be used for cloning the given repository over HTTPS.
public func httpsURL(for repository: Repository) -> GitURL {
let auth = tokenFromEnvironment(forServer: self).map { "\($0)@" } ?? ""
let scheme = url.scheme!
return GitURL("\(scheme)://\(auth)\(url.host!)/\(repository.owner)/\(repository.name).git")
}
/// The URL that should be used for cloning the given repository over SSH.
public func sshURL(for repository: Repository) -> GitURL {
return GitURL("ssh://git@\(url.host!)/\(repository.owner)/\(repository.name).git")
}
/// The URL for filing a new GitHub issue for the given repository.
public func newIssueURL(for repository: Repository) -> URL {
return URL(string: "\(self)/\(repository.owner)/\(repository.name)/issues/new")!
}
}
extension Repository {
/// Matches an identifier of the form "owner/name".
private static let nwoRegex = try! NSRegularExpression(pattern: "^([\\-\\.\\w]+)/([\\-\\.\\w]+)$", options: []) // swiftlint:disable:this force_try
/// Parses repository information out of a string of the form "owner/name"
/// for the github.com, or the form "http(s)://hostname/owner/name" for
/// Enterprise instances.
public static func fromIdentifier(_ identifier: String) -> Result<(Server, Repository), ScannableError> {
// ‘owner/name’ → GitHub.com
let range = NSRange(identifier.startIndex..., in: identifier)
if let match = nwoRegex.firstMatch(in: identifier, range: range) {
let owner = String(identifier[Range(match.range(at: 1), in: identifier)!])
let name = String(identifier[Range(match.range(at: 2), in: identifier)!])
return .success((.dotCom, self.init(owner: owner, name: strippingGitSuffix(name))))
}
// Hostname-based → GitHub Enterprise
guard
let url = URL(string: identifier),
let scheme = url.scheme,
// Reject `git` or `ssh` protocol as a `github` origin as it does not make sense.
// See https://github.com/Carthage/Carthage/issues/2379.
(scheme == "http" || scheme == "https"),
let host = url.host,
case var pathComponents = url.pathComponents.filter({ $0 != "/" }),
pathComponents.count >= 2,
case (let name, let owner) = (pathComponents.removeLast(), pathComponents.removeLast())
else {
return .failure(ScannableError(message: "invalid GitHub repository identifier \"\(identifier)\""))
}
// If the host name starts with “github.com”, that is not an enterprise
// one.
guard host != "github.com", host != "www.github.com" else {
return .success((.dotCom, self.init(owner: owner, name: strippingGitSuffix(name))))
}
let baseURL = url.deletingLastPathComponent().deletingLastPathComponent()
return .success((.enterprise(url: baseURL), self.init(owner: owner, name: strippingGitSuffix(name))))
}
}
extension Release {
/// The name of this release, with fallback to its tag when the name is an empty string or nil.
public var nameWithFallback: String {
if let name = name, !name.isEmpty {
return name
}
return tag
}
}
private func credentialsFromGit(forServer server: Server) -> (String, String)? {
let data = "url=\(server)".data(using: .utf8)!
return launchGitTask([ "credential", "fill" ], standardInput: SignalProducer(value: data))
.flatMap(.concat) { string in
return string.linesProducer
}
.reduce(into: [:]) { (values: inout [String: String], line: String) in
let parts = line
.split(maxSplits: 1, omittingEmptySubsequences: true) { $0 == "=" }
.map(String.init)
if parts.count >= 2 {
let key = parts[0]
let value = parts[1]
values[key] = value
}
}
.map { (values: [String: String]) -> (String, String)? in
if let username = values["username"], let password = values["password"] {
return (username, password)
}
return nil
}
.first()?
.value ?? nil // swiftlint:disable:this redundant_nil_coalescing
}
private func tokenFromEnvironment(forServer server: Server) -> String? {
let environment = ProcessInfo.processInfo.environment
if let accessTokenInput = environment["GITHUB_ACCESS_TOKEN"] {
// Treat the input as comma-separated series of domains and tokens.
// (e.g., `GITHUB_ACCESS_TOKEN="github.com=XXXXXXXXXXXXX,enterprise.local/ghe=YYYYYYYYY"`)
let records = accessTokenInput
.split(omittingEmptySubsequences: true) { $0 == "," }
.reduce(into: [:]) { (values: inout [String: String], record) in
let parts = record.split(maxSplits: 1, omittingEmptySubsequences: true) { $0 == "=" }.map(String.init)
switch parts.count {
case 1:
// If the input is provided as an access token itself, use the
// token for Github.com.
values["github.com"] = parts[0]
case 2:
let (server, token) = (parts[0], parts[1])
values[server] = token
default:
break
}
}
return records[server.url.host!]
}
return nil
}
extension Client {
convenience init(server: Server, isAuthenticated: Bool = true) {
if Client.userAgent == nil {
Client.userAgent = gitHubUserAgent()
}
let urlSession = URLSession.proxiedSession
if !isAuthenticated {
self.init(server, urlSession: urlSession)
} else if let token = tokenFromEnvironment(forServer: server) {
self.init(server, token: token, urlSession: urlSession)
} else if let (username, password) = credentialsFromGit(forServer: server) {
self.init(server, username: username, password: password, urlSession: urlSession)
} else {
self.init(server, urlSession: urlSession)
}
}
}
extension URLSession {
public static var proxiedSession: URLSession {
let configuration = URLSessionConfiguration.default
configuration.connectionProxyDictionary = Proxy.default.connectionProxyDictionary
let delegate = GitHubURLSessionDelegate()
return URLSession(configuration: configuration, delegate: delegate, delegateQueue: OperationQueue.current)
}
}
internal final class GitHubURLSessionDelegate: NSObject, URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
var newRequest = request
if let originalRequest = task.originalRequest {
if originalRequest.url?.host == request.url?.host {
if let auth = originalRequest.value(forHTTPHeaderField: "Authorization") {
newRequest.setValue(auth, forHTTPHeaderField: "Authorization")
}
}
}
completionHandler(newRequest)
}
}