Skip to content

Commit 8315230

Browse files
Merge pull request #3642 from SwiftPackageIndex/issue-3469-dependency-transition-25
Issue 3469 dependency transition 25
2 parents 42c9079 + b574060 commit 8315230

17 files changed

+438
-403
lines changed

Sources/App/Commands/Analyze.swift

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,9 @@ extension Analyze {
256256
static func clone(cacheDir: String, url: String) async throws {
257257
Current.logger().info("cloning \(url) to \(cacheDir)")
258258
@Dependency(\.fileManager) var fileManager
259-
try await Current.shell.run(command: .gitClone(url: URL(string: url)!, to: cacheDir),
260-
at: fileManager.checkoutsDirectory())
259+
@Dependency(\.shell) var shell
260+
try await shell.run(command: .gitClone(url: URL(string: url)!, to: cacheDir),
261+
at: fileManager.checkoutsDirectory())
261262
}
262263

263264

@@ -269,22 +270,22 @@ extension Analyze {
269270
/// - Throws: Shell errors
270271
static func fetch(cacheDir: String, branch: String, url: String) async throws {
271272
@Dependency(\.fileManager) var fileManager
273+
@Dependency(\.shell) var shell
272274
Current.logger().info("pulling \(url) in \(cacheDir)")
273275
// clean up stray lock files that might have remained from aborted commands
274276
for fileName in ["HEAD.lock", "index.lock"] {
275277
let filePath = cacheDir + "/.git/\(fileName)"
276278
if fileManager.fileExists(atPath: filePath) {
277279
Current.logger().info("Removing stale \(fileName) at path: \(filePath)")
278-
try await Current.shell.run(command: .removeFile(from: filePath))
280+
try await shell.run(command: .removeFile(from: filePath), at: .cwd)
279281
}
280282
}
281283
// git reset --hard to deal with stray .DS_Store files on macOS
282-
try await Current.shell.run(command: .gitReset(hard: true), at: cacheDir)
283-
try await Current.shell.run(command: .gitClean, at: cacheDir)
284-
try await Current.shell.run(command: .gitFetchAndPruneTags, at: cacheDir)
285-
try await Current.shell.run(command: .gitCheckout(branch: branch), at: cacheDir)
286-
try await Current.shell.run(command: .gitReset(to: branch, hard: true),
287-
at: cacheDir)
284+
try await shell.run(command: .gitReset(hard: true), at: cacheDir)
285+
try await shell.run(command: .gitClean, at: cacheDir)
286+
try await shell.run(command: .gitFetchAndPruneTags, at: cacheDir)
287+
try await shell.run(command: .gitCheckout(branch: branch), at: cacheDir)
288+
try await shell.run(command: .gitReset(to: branch, hard: true), at: cacheDir)
288289
}
289290

290291

@@ -293,6 +294,8 @@ extension Analyze {
293294
/// - package: `Package` to refresh
294295
static func refreshCheckout(package: Joined<Package, Repository>) async throws {
295296
@Dependency(\.fileManager) var fileManager
297+
@Dependency(\.shell) var shell
298+
296299
guard let cacheDir = fileManager.cacheDirectoryPath(for: package.model) else {
297300
throw AppError.invalidPackageCachePath(package.model.id, package.model.url)
298301
}
@@ -311,7 +314,7 @@ extension Analyze {
311314
url: package.model.url)
312315
} catch {
313316
Current.logger().info("fetch failed: \(error.localizedDescription)")
314-
try await Current.shell.run(command: .removeFile(from: cacheDir, arguments: ["-r", "-f"]))
317+
try await shell.run(command: .removeFile(from: cacheDir, arguments: ["-r", "-f"]), at: .cwd)
315318
try await clone(cacheDir: cacheDir, url: package.model.url)
316319
}
317320
} catch {
@@ -537,12 +540,13 @@ extension Analyze {
537540
/// - Returns: `Manifest` data
538541
static func dumpPackage(at path: String) async throws -> Manifest {
539542
@Dependency(\.fileManager) var fileManager
543+
@Dependency(\.shell) var shell
540544
guard fileManager.fileExists(atPath: path + "/Package.swift") else {
541545
// It's important to check for Package.swift - otherwise `dump-package` will go
542546
// up the tree through parent directories to find one
543547
throw AppError.invalidRevision(nil, "no Package.swift")
544548
}
545-
let json = try await Current.shell.run(command: .swiftDumpPackage, at: path)
549+
let json = try await shell.run(command: .swiftDumpPackage, at: path)
546550
return try JSONDecoder().decode(Manifest.self, from: Data(json.utf8))
547551
}
548552

@@ -561,12 +565,14 @@ extension Analyze {
561565
static func getPackageInfo(package: Joined<Package, Repository>, version: Version) async throws -> PackageInfo {
562566
// check out version in cache directory
563567
@Dependency(\.fileManager) var fileManager
568+
@Dependency(\.shell) var shell
569+
564570
guard let cacheDir = fileManager.cacheDirectoryPath(for: package.model) else {
565571
throw AppError.invalidPackageCachePath(package.model.id,
566572
package.model.url)
567573
}
568574

569-
try await Current.shell.run(command: .gitCheckout(branch: version.reference.description), at: cacheDir)
575+
try await shell.run(command: .gitCheckout(branch: version.reference.description), at: cacheDir)
570576

571577
do {
572578
let packageManifest = try await dumpPackage(at: cacheDir)

Sources/App/Core/AppEnvironment.swift

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import FoundationNetworking
2525
struct AppEnvironment: Sendable {
2626
var logger: @Sendable () -> Logger
2727
var setLogger: @Sendable (Logger) -> Void
28-
var shell: Shell
2928
}
3029

3130

@@ -34,35 +33,7 @@ extension AppEnvironment {
3433

3534
static let live = AppEnvironment(
3635
logger: { logger },
37-
setLogger: { logger in Self.logger = logger },
38-
shell: .live
39-
)
40-
}
41-
42-
43-
44-
struct Shell: Sendable {
45-
var run: @Sendable (ShellOutCommand, String) async throws -> String
46-
47-
// also provide pass-through methods to preserve argument labels
48-
@discardableResult
49-
func run(command: ShellOutCommand, at path: String = ".") async throws -> String {
50-
do {
51-
return try await run(command, path)
52-
} catch {
53-
// re-package error to capture more information
54-
throw AppError.shellCommandFailed(command.description, path, error.localizedDescription)
55-
}
56-
}
57-
58-
static let live: Self = .init(
59-
run: {
60-
let res = try await ShellOut.shellOut(to: $0, at: $1, logger: Current.logger())
61-
if !res.stderr.isEmpty {
62-
Current.logger().warning("stderr: \(res.stderr)")
63-
}
64-
return res.stdout
65-
}
36+
setLogger: { logger in Self.logger = logger }
6637
)
6738
}
6839

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Dependencies
16+
import DependenciesMacros
17+
import ShellOut
18+
19+
20+
@DependencyClient
21+
struct ShellClient {
22+
var run: @Sendable (ShellOutCommand, String) async throws -> String
23+
}
24+
25+
26+
extension ShellClient {
27+
@discardableResult
28+
func run(command: ShellOutCommand, at path: String) async throws -> String {
29+
try await run(command, path)
30+
}
31+
}
32+
33+
34+
extension String {
35+
static let cwd = "."
36+
}
37+
38+
39+
extension ShellClient: DependencyKey {
40+
static var liveValue: Self {
41+
.init(
42+
run: { command, path in
43+
do {
44+
let res = try await ShellOut.shellOut(to: command, at: path, logger: Current.logger())
45+
if !res.stderr.isEmpty {
46+
Current.logger().warning("stderr: \(res.stderr)")
47+
}
48+
return res.stdout
49+
} catch {
50+
// re-package error to capture more information
51+
throw AppError.shellCommandFailed(command.description, path, error.localizedDescription)
52+
}
53+
}
54+
)
55+
}
56+
}
57+
58+
59+
extension ShellClient: TestDependencyKey {
60+
static var testValue: Self { .init() }
61+
}
62+
63+
64+
extension DependencyValues {
65+
var shell: ShellClient {
66+
get { self[ShellClient.self] }
67+
set { self[ShellClient.self] = newValue }
68+
}
69+
}

Sources/App/Core/Git.swift

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
// limitations under the License.
1414

1515
import Foundation
16+
17+
import Dependencies
1618
import SemanticVersion
1719
import ShellOut
1820

@@ -26,16 +28,18 @@ enum Git {
2628
}
2729

2830
static func commitCount(at path: String) async throws -> Int {
29-
let res = try await Current.shell.run(command: .gitCommitCount, at: path)
31+
@Dependency(\.shell) var shell
32+
let res = try await shell.run(command: .gitCommitCount, at: path)
3033
guard let count = Int(res) else {
3134
throw Error.invalidInteger
3235
}
3336
return count
3437
}
3538

3639
static func firstCommitDate(at path: String) async throws -> Date {
40+
@Dependency(\.shell) var shell
3741
let res = String(
38-
try await Current.shell.run(command: .gitFirstCommitDate, at: path)
42+
try await shell.run(command: .gitFirstCommitDate, at: path)
3943
.trimming { $0 == Character("\"") }
4044
)
4145
guard let timestamp = TimeInterval(res) else {
@@ -45,8 +49,9 @@ enum Git {
4549
}
4650

4751
static func lastCommitDate(at path: String) async throws -> Date {
52+
@Dependency(\.shell) var shell
4853
let res = String(
49-
try await Current.shell.run(command: .gitLastCommitDate, at: path)
54+
try await shell.run(command: .gitLastCommitDate, at: path)
5055
.trimming { $0 == Character("\"") }
5156
)
5257
guard let timestamp = TimeInterval(res) else {
@@ -56,27 +61,30 @@ enum Git {
5661
}
5762

5863
static func getTags(at path: String) async throws -> [Reference] {
59-
let tags = try await Current.shell.run(command: .gitListTags, at: path)
64+
@Dependency(\.shell) var shell
65+
let tags = try await shell.run(command: .gitListTags, at: path)
6066
return tags.split(separator: "\n")
6167
.map(String.init)
6268
.compactMap { tag in SemanticVersion(tag).map { ($0, tag) } }
6369
.map { Reference.tag($0, $1) }
6470
}
6571

6672
static func hasBranch(_ reference: Reference, at path: String) async throws -> Bool {
73+
@Dependency(\.shell) var shell
6774
guard let branchName = reference.branchName else { return false }
6875
do {
69-
_ = try await Current.shell.run(command: .gitHasBranch(branchName), at: path)
76+
_ = try await shell.run(command: .gitHasBranch(branchName), at: path)
7077
return true
7178
} catch {
7279
return false
7380
}
7481
}
7582

7683
static func revisionInfo(_ reference: Reference, at path: String) async throws -> RevisionInfo {
84+
@Dependency(\.shell) var shell
7785
let separator = "-"
7886
let res = String(
79-
try await Current.shell.run(command: .gitRevisionInfo(reference: reference, separator: separator),
87+
try await shell.run(command: .gitRevisionInfo(reference: reference, separator: separator),
8088
at: path)
8189
.trimming { $0 == Character("\"") }
8290
)
@@ -92,7 +100,8 @@ enum Git {
92100
}
93101

94102
static func shortlog(at path: String) async throws -> String {
95-
try await Current.shell.run(command: .gitShortlog, at: path)
103+
@Dependency(\.shell) var shell
104+
return try await shell.run(command: .gitShortlog, at: path)
96105
}
97106

98107
struct RevisionInfo: Equatable {

Tests/AppTests/AnalyzeErrorTests.swift

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,6 @@ final class AnalyzeErrorTests: AppTestCase {
7272
Repository(package: pkgs[0], defaultBranch: "main", name: "1", owner: "foo"),
7373
Repository(package: pkgs[1], defaultBranch: "main", name: "2", owner: "foo"),
7474
].save(on: app.db)
75-
76-
Current.shell.run = Self.defaultShellRun
7775
}
7876

7977
override func invokeTest() {
@@ -106,6 +104,7 @@ final class AnalyzeErrorTests: AppTestCase {
106104
$0.httpClient.mastodonPost = { @Sendable [socialPosts = self.socialPosts] message in
107105
socialPosts.withValue { $0.append(message) }
108106
}
107+
$0.shell.run = Self.defaultShellRun
109108
} operation: {
110109
super.invokeTest()
111110
}
@@ -115,8 +114,7 @@ final class AnalyzeErrorTests: AppTestCase {
115114
try await withDependencies {
116115
$0.environment.loadSPIManifest = { _ in nil }
117116
$0.fileManager.fileExists = { @Sendable _ in true }
118-
} operation: {
119-
Current.shell.run = { @Sendable cmd, path in
117+
$0.shell.run = { @Sendable cmd, path in
120118
switch cmd {
121119
case _ where cmd.description.contains("git clone https://github.com/foo/1"):
122120
throw SimulatedError()
@@ -128,7 +126,7 @@ final class AnalyzeErrorTests: AppTestCase {
128126
return try Self.defaultShellRun(cmd, path)
129127
}
130128
}
131-
129+
} operation: {
132130
// MUT
133131
try await Analyze.analyze(client: app.client, database: app.db, mode: .limit(10))
134132

@@ -172,9 +170,7 @@ final class AnalyzeErrorTests: AppTestCase {
172170
try await withDependencies {
173171
$0.environment.loadSPIManifest = { _ in nil }
174172
$0.fileManager.fileExists = { @Sendable _ in true }
175-
} operation: {
176-
// setup
177-
Current.shell.run = { @Sendable cmd, path in
173+
$0.shell.run = { @Sendable cmd, path in
178174
switch cmd {
179175
case .gitCheckout(branch: "main", quiet: true) where path.hasSuffix("foo-1"):
180176
throw SimulatedError()
@@ -183,7 +179,7 @@ final class AnalyzeErrorTests: AppTestCase {
183179
return try Self.defaultShellRun(cmd, path)
184180
}
185181
}
186-
182+
} operation: {
187183
// MUT
188184
try await Analyze.analyze(client: app.client, database: app.db, mode: .limit(10))
189185

0 commit comments

Comments
 (0)