Skip to content

Commit 6dffdca

Browse files
committed
Support explicit embedded resources and multi-env git
1 parent cec3710 commit 6dffdca

14 files changed

Lines changed: 154 additions & 90 deletions

Package.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,19 @@ let package = Package(
2525
]),
2626
.target(
2727
name: "SwiftGitResourcesArm64",
28+
dependencies: ["SwiftGit"],
2829
resources: [
2930
.copy("Resource/git-instance.bundle")
3031
]),
3132
.target(
3233
name: "SwiftGitResourcesX86_64",
34+
dependencies: ["SwiftGit"],
3335
resources: [
3436
.copy("Resource/git-instance.bundle")
3537
]),
3638
.target(
3739
name: "SwiftGitResourcesUniversal",
40+
dependencies: ["SwiftGit"],
3841
resources: [
3942
.copy("Resource/git-instance.bundle")
4043
]),

README.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ Important modules (quick code map)
7878
- Use `Git.data(...)` or `Git.run(...)` which delegate to `Shell.Instance`.
7979

8080
- `GitEnvironment` (`Sources/SwiftGit/Custom/GitEnvironment.swift`)
81-
- Selects between embedded git (`.embed`), system git (`.system`), or custom folder.
81+
- Selects between embedded git (`.embed(...)`), system git (`.system`), or custom folder.
8282
- Exposes environment variables used to run git (e.g. `GIT_EXEC_PATH`).
8383

8484
- `Git` (`Sources/SwiftGit/Custom/Git.swift`)
@@ -97,8 +97,18 @@ Embedded git bundle
9797
- `SwiftGitArm64``Sources/SwiftGitResourcesArm64/Resource/git-instance.bundle`
9898
- `SwiftGitX86_64``Sources/SwiftGitResourcesX86_64/Resource/git-instance.bundle`
9999
- `SwiftGitUniversal``Sources/SwiftGitResourcesUniversal/Resource/git-instance.bundle`
100-
- `SwiftGit` (base product) no longer embeds a bundle; `GitEnvironment.Style.embed` will only succeed if one of the resource products is linked.
101-
- `GitEnvironment.Style.auto` now maps to system git only; use `.embed` explicitly when you add a resource product.
100+
- `SwiftGit` (base product) no longer embeds a bundle; `.embed(.arm64/.x86_64/.universal)` works only when the corresponding resource product is linked.
101+
- `GitEnvironment.Style.auto` now maps to system git only; use `.embed(...)` explicitly when you add a resource product.
102+
103+
Example usage (arm64):
104+
105+
```swift
106+
import SwiftGit
107+
import SwiftGitResourcesArm64
108+
109+
let env = try GitEnvironment(type: .embed(.arm64))
110+
let git = Git(environment: env)
111+
```
102112
- Agents and tests should not modify files under the bundles. Use `GitEnvironment.Style.custom(URL)` to point to an alternate git distribution.
103113
- To generate updated bundles, run `tools/update-git-bundle.sh` with the desired `--archs` and copy the result into the corresponding `Sources/SwiftGitResources*/Resource/` directory.
104114
- If a bundle looks unusually large, run `tools/fix-git-bundle-links.sh` to relink duplicate `git-*` binaries and shrink the footprint.
@@ -109,7 +119,7 @@ Tests and integration tests
109119
- Tests live under `Tests/SwiftGitTests/`.
110120
- Unit tests: parsing and helpers (safe parsing of `git` output, command splitting).
111121
- Integration tests: initialize temporary repos and run real git workflows (init, add, commit, tag, branch, stash, rebase, plumbing commands like `write-tree`).
112-
- Integration tests try to use the embedded git first and fall back to system git. If neither is available they skip.
122+
- Integration tests use system git; if git is unavailable they skip.
113123
- Helper examples: `Tests/SwiftGitTests/IntegrationTests.swift`, `PlumbingTests.swift`, `ShowCommitResultTests.swift`.
114124

115125
Design & safety notes

Sources/SwiftGit/Custom/Git.swift

Lines changed: 90 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,20 @@ public class Git {
2020
}
2121
}
2222

23-
public let environment: GitEnvironment
23+
public private(set) var environment: GitEnvironment
24+
private let environments: [GitEnvironment]
25+
private var activeEnvironment: GitEnvironment?
2426
public var shell = Shell.Instance()
2527

2628
public init(environment: GitEnvironment) {
2729
self.environment = environment
30+
self.environments = [environment]
31+
}
32+
33+
public init(environments: [GitEnvironment]) {
34+
precondition(!environments.isEmpty, "Git requires at least one GitEnvironment")
35+
self.environments = environments
36+
self.environment = environments[0]
2837
}
2938

3039
private var cancellables = Set<AnyCancellable>()
@@ -35,23 +44,32 @@ extension Git {
3544
public func dataPublisher(_ commands: [String], context: Shell.Context? = nil)
3645
-> AnyPublisher<Data, Error>
3746
{
38-
self.triggerOfBeforeRun(commands)
47+
let env: GitEnvironment
48+
do {
49+
env = try resolveEnvironment()
50+
} catch {
51+
return Fail(error: error).eraseToAnyPublisher()
52+
}
53+
self.triggerOfBeforeRun(commands, environment: env)
3954
return
4055
shell
4156
.dataPublisher(
4257
Shell.Arguments(
43-
exec: self.environment.resource.executableURL,
58+
exec: env.resource.executableURL,
4459
commands: commands,
45-
context: self.deal(context: context))
60+
context: self.deal(context: context, environment: env))
4661
)
4762
.map({ [weak self] data in
4863
guard let self = self else { return data }
49-
self.triggerOfAfterRun(commands, data: data)
64+
self.triggerOfAfterRun(commands, data: data, environment: env)
5065
return data
5166
})
5267
.mapError({ [weak self] error in
5368
guard let self = self else { return error }
54-
_ = self.triggerOfAfterRun(commands, message: error.localizedDescription)
69+
_ = self.triggerOfAfterRun(
70+
commands,
71+
message: error.localizedDescription,
72+
environment: env)
5573
return error
5674
})
5775
.eraseToAnyPublisher()
@@ -124,18 +142,21 @@ extension Git {
124142
@discardableResult
125143
public func data(_ commands: [String], context: Shell.Context? = nil) async throws -> Data {
126144
do {
127-
triggerOfBeforeRun(commands)
145+
let env = try resolveEnvironment()
146+
triggerOfBeforeRun(commands, environment: env)
128147
let data = try await shell.data(
129148
.init(
130-
exec: environment.resource.executableURL,
149+
exec: env.resource.executableURL,
131150
commands: commands,
132-
context: deal(context: context)))
133-
triggerOfAfterRun(commands, data: data)
151+
context: deal(context: context, environment: env)))
152+
triggerOfAfterRun(commands, data: data, environment: env)
134153
return data
135154
} catch GitError.processFatal(let message) {
136-
throw triggerOfAfterRun(commands, message: message)
155+
let env = try resolveEnvironment()
156+
throw triggerOfAfterRun(commands, message: message, environment: env)
137157
} catch {
138-
throw triggerOfAfterRun(commands, message: error.localizedDescription)
158+
let env = try resolveEnvironment()
159+
throw triggerOfAfterRun(commands, message: error.localizedDescription, environment: env)
139160
}
140161
}
141162

@@ -166,28 +187,29 @@ extension Git {
166187
progress: @escaping @Sendable (GitProgress) -> GitProgressAction
167188
) async throws -> String {
168189
try Task.checkCancellation()
169-
triggerOfBeforeRun(commands)
190+
let env = try resolveEnvironment()
191+
triggerOfBeforeRun(commands, environment: env)
170192
let parser = GitProgressParser(progress: progress)
171193
do {
172194
let data = try await shell.data(
173195
.init(
174-
exec: environment.resource.executableURL,
196+
exec: env.resource.executableURL,
175197
commands: commands,
176-
context: deal(context: context)
198+
context: deal(context: context, environment: env)
177199
),
178200
onStdout: { parser.handle($0) },
179201
onStderr: { parser.handle($0) },
180202
shouldCancel: { parser.isCancelRequested || Task.isCancelled }
181203
)
182204
parser.finish()
183-
triggerOfAfterRun(commands, data: data)
205+
triggerOfAfterRun(commands, data: data, environment: env)
184206
return String(data: data, encoding: .utf8)?.trimmingCharacters(in: .newlines) ?? ""
185207
} catch is CancellationError {
186208
throw CancellationError()
187209
} catch GitError.processFatal(let message) {
188-
throw triggerOfAfterRun(commands, message: message)
210+
throw triggerOfAfterRun(commands, message: message, environment: env)
189211
} catch {
190-
throw triggerOfAfterRun(commands, message: error.localizedDescription)
212+
throw triggerOfAfterRun(commands, message: error.localizedDescription, environment: env)
191213
}
192214
}
193215

@@ -198,18 +220,21 @@ extension Git {
198220
@discardableResult
199221
public func data(_ commands: [String], context: Shell.Context? = nil) throws -> Data {
200222
do {
201-
triggerOfBeforeRun(commands)
223+
let env = try resolveEnvironment()
224+
triggerOfBeforeRun(commands, environment: env)
202225
let data = try shell.data(
203226
.init(
204-
exec: environment.resource.executableURL,
227+
exec: env.resource.executableURL,
205228
commands: commands,
206-
context: deal(context: context)))
207-
triggerOfAfterRun(commands, data: data)
229+
context: deal(context: context, environment: env)))
230+
triggerOfAfterRun(commands, data: data, environment: env)
208231
return data
209232
} catch GitError.processFatal(let message) {
210-
throw triggerOfAfterRun(commands, message: message)
233+
let env = try resolveEnvironment()
234+
throw triggerOfAfterRun(commands, message: message, environment: env)
211235
} catch {
212-
throw triggerOfAfterRun(commands, message: error.localizedDescription)
236+
let env = try resolveEnvironment()
237+
throw triggerOfAfterRun(commands, message: error.localizedDescription, environment: env)
213238
}
214239
}
215240

@@ -245,7 +270,7 @@ extension Git {
245270

246271
extension Git {
247272

248-
fileprivate func deal(context: Shell.Context?) -> Shell.Context {
273+
fileprivate func deal(context: Shell.Context?, environment: GitEnvironment) -> Shell.Context {
249274
var environmentDict = context?.environment ?? [:]
250275
environment.variables.forEach { item in
251276
environmentDict[item.key] = item.value
@@ -258,7 +283,11 @@ extension Git {
258283
return ctx
259284
}
260285

261-
fileprivate func triggerOfAfterRun(_ commands: [String], data: Data) {
286+
fileprivate func triggerOfAfterRun(
287+
_ commands: [String],
288+
data: Data,
289+
environment: GitEnvironment
290+
) {
262291
if let triggers = environment.triggersMap[.afterRun] {
263292
let content = GitTrigger.Content(commands: commands, data: data)
264293
triggers.forEach({ item in
@@ -267,15 +296,19 @@ extension Git {
267296
}
268297
}
269298

270-
fileprivate func triggerOfAfterRun(_ commands: [String], message: String) -> GitError {
299+
fileprivate func triggerOfAfterRun(
300+
_ commands: [String],
301+
message: String,
302+
environment: GitEnvironment
303+
) -> GitError {
271304
let error = GitTrigger.Error(commands: commands, message: message)
272305
environment.triggersMap[.afterRun]?.forEach({ item in
273306
item.action(.failure(error))
274307
})
275308
return GitError.processFatal(error.message)
276309
}
277310

278-
fileprivate func triggerOfBeforeRun(_ commands: [String]) {
311+
fileprivate func triggerOfBeforeRun(_ commands: [String], environment: GitEnvironment) {
279312
if let triggers = environment.triggersMap[.beforeRun] {
280313
let content = GitTrigger.Content(commands: commands, data: Data())
281314
triggers.forEach({ item in
@@ -285,3 +318,32 @@ extension Git {
285318
}
286319

287320
}
321+
322+
extension Git {
323+
324+
private func resolveEnvironment() throws -> GitEnvironment {
325+
if let active = activeEnvironment {
326+
return active
327+
}
328+
for env in environments {
329+
if isUsable(env) {
330+
activeEnvironment = env
331+
environment = env
332+
return env
333+
}
334+
}
335+
throw GitError.noGitInstanceFoundOnSystem
336+
}
337+
338+
private func isUsable(_ env: GitEnvironment) -> Bool {
339+
let execPath = env.resource.executableURL.path
340+
guard FileManager.default.isExecutableFile(atPath: execPath) else {
341+
return false
342+
}
343+
if let execPath = env.resource.envExecPath {
344+
return FileManager.default.fileExists(atPath: execPath)
345+
}
346+
return true
347+
}
348+
349+
}

Sources/SwiftGit/Custom/GitEnvironment.swift

Lines changed: 12 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ extension GitEnvironment {
5151
}
5252

5353
public enum Style {
54-
case embed
54+
case embed(EmbeddedResource)
5555
case system
5656
case custom(_ url: URL)
5757
case auto
@@ -64,12 +64,13 @@ extension GitEnvironment {
6464
triggers: [GitTrigger] = []
6565
) throws {
6666
switch type {
67-
case .embed:
67+
case .embed(let embedded):
6868
guard
69-
let resourceBundle = Self.findEmbeddedResourceBundle(),
70-
let embeddedURL = resourceBundle.url(forResource: "git-instance", withExtension: "bundle"),
71-
let bundle = Bundle(url: embeddedURL),
72-
let resource = Resource(bundle: bundle)
69+
let embeddedURL = embedded.bundle.url(
70+
forResource: "git-instance",
71+
withExtension: "bundle"),
72+
let resourceBundle = Bundle(url: embeddedURL),
73+
let resource = Resource(bundle: resourceBundle)
7374
else {
7475
throw GitError.unableLoadEmbeddGitInstance
7576
}
@@ -112,42 +113,12 @@ extension GitEnvironment {
112113

113114
extension GitEnvironment {
114115

115-
private static func findEmbeddedResourceBundle() -> Bundle? {
116-
let candidates: [String]
117-
#if arch(arm64)
118-
candidates = [
119-
"SwiftGitResourcesArm64",
120-
"SwiftGitResourcesUniversal",
121-
"SwiftGitResourcesX86_64",
122-
"SwiftGit"
123-
]
124-
#elseif arch(x86_64)
125-
candidates = [
126-
"SwiftGitResourcesX86_64",
127-
"SwiftGitResourcesUniversal",
128-
"SwiftGitResourcesArm64",
129-
"SwiftGit"
130-
]
131-
#else
132-
candidates = [
133-
"SwiftGitResourcesUniversal",
134-
"SwiftGitResourcesArm64",
135-
"SwiftGitResourcesX86_64",
136-
"SwiftGit"
137-
]
138-
#endif
139-
140-
let bundles = Bundle.allBundles + Bundle.allFrameworks
141-
for name in candidates {
142-
if let url = Bundle.main.url(forResource: name, withExtension: "bundle"),
143-
let bundle = Bundle(url: url) {
144-
return bundle
145-
}
146-
if let bundle = bundles.first(where: { $0.bundleURL.lastPathComponent == "\(name).bundle" }) {
147-
return bundle
148-
}
116+
public struct EmbeddedResource {
117+
public let bundle: Bundle
118+
119+
public init(bundle: Bundle) {
120+
self.bundle = bundle
149121
}
150-
return nil
151122
}
152123

153124
public struct Resource {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import Foundation
2+
import SwiftGit
23

34
public enum SwiftGitResourcesArm64 {
45
public static let bundle = Bundle.module
56
}
7+
8+
public extension GitEnvironment.EmbeddedResource {
9+
static var arm64: GitEnvironment.EmbeddedResource {
10+
GitEnvironment.EmbeddedResource(bundle: SwiftGitResourcesArm64.bundle)
11+
}
12+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import Foundation
2+
import SwiftGit
23

34
public enum SwiftGitResourcesUniversal {
45
public static let bundle = Bundle.module
56
}
7+
8+
public extension GitEnvironment.EmbeddedResource {
9+
static var universal: GitEnvironment.EmbeddedResource {
10+
GitEnvironment.EmbeddedResource(bundle: SwiftGitResourcesUniversal.bundle)
11+
}
12+
}

0 commit comments

Comments
 (0)