Skip to content

Commit

Permalink
Merge pull request #3642 from SwiftPackageIndex/issue-3469-dependency…
Browse files Browse the repository at this point in the history
…-transition-25

Issue 3469 dependency transition 25
  • Loading branch information
finestructure authored Jan 29, 2025
2 parents 42c9079 + b574060 commit 8315230
Show file tree
Hide file tree
Showing 17 changed files with 438 additions and 403 deletions.
30 changes: 18 additions & 12 deletions Sources/App/Commands/Analyze.swift
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,9 @@ extension Analyze {
static func clone(cacheDir: String, url: String) async throws {
Current.logger().info("cloning \(url) to \(cacheDir)")
@Dependency(\.fileManager) var fileManager
try await Current.shell.run(command: .gitClone(url: URL(string: url)!, to: cacheDir),
at: fileManager.checkoutsDirectory())
@Dependency(\.shell) var shell
try await shell.run(command: .gitClone(url: URL(string: url)!, to: cacheDir),
at: fileManager.checkoutsDirectory())
}


Expand All @@ -269,22 +270,22 @@ extension Analyze {
/// - Throws: Shell errors
static func fetch(cacheDir: String, branch: String, url: String) async throws {
@Dependency(\.fileManager) var fileManager
@Dependency(\.shell) var shell
Current.logger().info("pulling \(url) in \(cacheDir)")
// clean up stray lock files that might have remained from aborted commands
for fileName in ["HEAD.lock", "index.lock"] {
let filePath = cacheDir + "/.git/\(fileName)"
if fileManager.fileExists(atPath: filePath) {
Current.logger().info("Removing stale \(fileName) at path: \(filePath)")
try await Current.shell.run(command: .removeFile(from: filePath))
try await shell.run(command: .removeFile(from: filePath), at: .cwd)
}
}
// git reset --hard to deal with stray .DS_Store files on macOS
try await Current.shell.run(command: .gitReset(hard: true), at: cacheDir)
try await Current.shell.run(command: .gitClean, at: cacheDir)
try await Current.shell.run(command: .gitFetchAndPruneTags, at: cacheDir)
try await Current.shell.run(command: .gitCheckout(branch: branch), at: cacheDir)
try await Current.shell.run(command: .gitReset(to: branch, hard: true),
at: cacheDir)
try await shell.run(command: .gitReset(hard: true), at: cacheDir)
try await shell.run(command: .gitClean, at: cacheDir)
try await shell.run(command: .gitFetchAndPruneTags, at: cacheDir)
try await shell.run(command: .gitCheckout(branch: branch), at: cacheDir)
try await shell.run(command: .gitReset(to: branch, hard: true), at: cacheDir)
}


Expand All @@ -293,6 +294,8 @@ extension Analyze {
/// - package: `Package` to refresh
static func refreshCheckout(package: Joined<Package, Repository>) async throws {
@Dependency(\.fileManager) var fileManager
@Dependency(\.shell) var shell

guard let cacheDir = fileManager.cacheDirectoryPath(for: package.model) else {
throw AppError.invalidPackageCachePath(package.model.id, package.model.url)
}
Expand All @@ -311,7 +314,7 @@ extension Analyze {
url: package.model.url)
} catch {
Current.logger().info("fetch failed: \(error.localizedDescription)")
try await Current.shell.run(command: .removeFile(from: cacheDir, arguments: ["-r", "-f"]))
try await shell.run(command: .removeFile(from: cacheDir, arguments: ["-r", "-f"]), at: .cwd)
try await clone(cacheDir: cacheDir, url: package.model.url)
}
} catch {
Expand Down Expand Up @@ -537,12 +540,13 @@ extension Analyze {
/// - Returns: `Manifest` data
static func dumpPackage(at path: String) async throws -> Manifest {
@Dependency(\.fileManager) var fileManager
@Dependency(\.shell) var shell
guard fileManager.fileExists(atPath: path + "/Package.swift") else {
// It's important to check for Package.swift - otherwise `dump-package` will go
// up the tree through parent directories to find one
throw AppError.invalidRevision(nil, "no Package.swift")
}
let json = try await Current.shell.run(command: .swiftDumpPackage, at: path)
let json = try await shell.run(command: .swiftDumpPackage, at: path)
return try JSONDecoder().decode(Manifest.self, from: Data(json.utf8))
}

Expand All @@ -561,12 +565,14 @@ extension Analyze {
static func getPackageInfo(package: Joined<Package, Repository>, version: Version) async throws -> PackageInfo {
// check out version in cache directory
@Dependency(\.fileManager) var fileManager
@Dependency(\.shell) var shell

guard let cacheDir = fileManager.cacheDirectoryPath(for: package.model) else {
throw AppError.invalidPackageCachePath(package.model.id,
package.model.url)
}

try await Current.shell.run(command: .gitCheckout(branch: version.reference.description), at: cacheDir)
try await shell.run(command: .gitCheckout(branch: version.reference.description), at: cacheDir)

do {
let packageManifest = try await dumpPackage(at: cacheDir)
Expand Down
31 changes: 1 addition & 30 deletions Sources/App/Core/AppEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import FoundationNetworking
struct AppEnvironment: Sendable {
var logger: @Sendable () -> Logger
var setLogger: @Sendable (Logger) -> Void
var shell: Shell
}


Expand All @@ -34,35 +33,7 @@ extension AppEnvironment {

static let live = AppEnvironment(
logger: { logger },
setLogger: { logger in Self.logger = logger },
shell: .live
)
}



struct Shell: Sendable {
var run: @Sendable (ShellOutCommand, String) async throws -> String

// also provide pass-through methods to preserve argument labels
@discardableResult
func run(command: ShellOutCommand, at path: String = ".") async throws -> String {
do {
return try await run(command, path)
} catch {
// re-package error to capture more information
throw AppError.shellCommandFailed(command.description, path, error.localizedDescription)
}
}

static let live: Self = .init(
run: {
let res = try await ShellOut.shellOut(to: $0, at: $1, logger: Current.logger())
if !res.stderr.isEmpty {
Current.logger().warning("stderr: \(res.stderr)")
}
return res.stdout
}
setLogger: { logger in Self.logger = logger }
)
}

Expand Down
69 changes: 69 additions & 0 deletions Sources/App/Core/Dependencies/ShellClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Dependencies
import DependenciesMacros
import ShellOut


@DependencyClient
struct ShellClient {
var run: @Sendable (ShellOutCommand, String) async throws -> String
}


extension ShellClient {
@discardableResult
func run(command: ShellOutCommand, at path: String) async throws -> String {
try await run(command, path)
}
}


extension String {
static let cwd = "."
}


extension ShellClient: DependencyKey {
static var liveValue: Self {
.init(
run: { command, path in
do {
let res = try await ShellOut.shellOut(to: command, at: path, logger: Current.logger())
if !res.stderr.isEmpty {
Current.logger().warning("stderr: \(res.stderr)")
}
return res.stdout
} catch {
// re-package error to capture more information
throw AppError.shellCommandFailed(command.description, path, error.localizedDescription)
}
}
)
}
}


extension ShellClient: TestDependencyKey {
static var testValue: Self { .init() }
}


extension DependencyValues {
var shell: ShellClient {
get { self[ShellClient.self] }
set { self[ShellClient.self] = newValue }
}
}
23 changes: 16 additions & 7 deletions Sources/App/Core/Git.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
// limitations under the License.

import Foundation

import Dependencies
import SemanticVersion
import ShellOut

Expand All @@ -26,16 +28,18 @@ enum Git {
}

static func commitCount(at path: String) async throws -> Int {
let res = try await Current.shell.run(command: .gitCommitCount, at: path)
@Dependency(\.shell) var shell
let res = try await shell.run(command: .gitCommitCount, at: path)
guard let count = Int(res) else {
throw Error.invalidInteger
}
return count
}

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

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

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

static func hasBranch(_ reference: Reference, at path: String) async throws -> Bool {
@Dependency(\.shell) var shell
guard let branchName = reference.branchName else { return false }
do {
_ = try await Current.shell.run(command: .gitHasBranch(branchName), at: path)
_ = try await shell.run(command: .gitHasBranch(branchName), at: path)
return true
} catch {
return false
}
}

static func revisionInfo(_ reference: Reference, at path: String) async throws -> RevisionInfo {
@Dependency(\.shell) var shell
let separator = "-"
let res = String(
try await Current.shell.run(command: .gitRevisionInfo(reference: reference, separator: separator),
try await shell.run(command: .gitRevisionInfo(reference: reference, separator: separator),
at: path)
.trimming { $0 == Character("\"") }
)
Expand All @@ -92,7 +100,8 @@ enum Git {
}

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

struct RevisionInfo: Equatable {
Expand Down
14 changes: 5 additions & 9 deletions Tests/AppTests/AnalyzeErrorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ final class AnalyzeErrorTests: AppTestCase {
Repository(package: pkgs[0], defaultBranch: "main", name: "1", owner: "foo"),
Repository(package: pkgs[1], defaultBranch: "main", name: "2", owner: "foo"),
].save(on: app.db)

Current.shell.run = Self.defaultShellRun
}

override func invokeTest() {
Expand Down Expand Up @@ -106,6 +104,7 @@ final class AnalyzeErrorTests: AppTestCase {
$0.httpClient.mastodonPost = { @Sendable [socialPosts = self.socialPosts] message in
socialPosts.withValue { $0.append(message) }
}
$0.shell.run = Self.defaultShellRun
} operation: {
super.invokeTest()
}
Expand All @@ -115,8 +114,7 @@ final class AnalyzeErrorTests: AppTestCase {
try await withDependencies {
$0.environment.loadSPIManifest = { _ in nil }
$0.fileManager.fileExists = { @Sendable _ in true }
} operation: {
Current.shell.run = { @Sendable cmd, path in
$0.shell.run = { @Sendable cmd, path in
switch cmd {
case _ where cmd.description.contains("git clone https://github.com/foo/1"):
throw SimulatedError()
Expand All @@ -128,7 +126,7 @@ final class AnalyzeErrorTests: AppTestCase {
return try Self.defaultShellRun(cmd, path)
}
}

} operation: {
// MUT
try await Analyze.analyze(client: app.client, database: app.db, mode: .limit(10))

Expand Down Expand Up @@ -172,9 +170,7 @@ final class AnalyzeErrorTests: AppTestCase {
try await withDependencies {
$0.environment.loadSPIManifest = { _ in nil }
$0.fileManager.fileExists = { @Sendable _ in true }
} operation: {
// setup
Current.shell.run = { @Sendable cmd, path in
$0.shell.run = { @Sendable cmd, path in
switch cmd {
case .gitCheckout(branch: "main", quiet: true) where path.hasSuffix("foo-1"):
throw SimulatedError()
Expand All @@ -183,7 +179,7 @@ final class AnalyzeErrorTests: AppTestCase {
return try Self.defaultShellRun(cmd, path)
}
}

} operation: {
// MUT
try await Analyze.analyze(client: app.client, database: app.db, mode: .limit(10))

Expand Down
Loading

0 comments on commit 8315230

Please sign in to comment.