Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Sources/CoreCommands/Options.swift
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,12 @@ public struct ResolverOptions: ParsableArguments {
@Option(help: "Default registry URL to use, instead of the registries.json configuration file.")
public var defaultRegistryURL: URL?

/// Whether to use Git LFS for large file support.
@Flag(name: .customLong("experimental-git-lfs"),
inversion: .prefixedEnableDisable,
help: "Whether to use Git LFS for large file support.")
public var useGitLFS: Bool = false

public enum SourceControlToRegistryDependencyTransformation: EnumerableFlag {
case disabled
case identity
Expand Down
1 change: 1 addition & 0 deletions Sources/CoreCommands/SwiftCommandState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,7 @@ public final class SwiftCommandState {
usePrebuilts: self.options.caching.usePrebuilts,
prebuiltsDownloadURL: options.caching.prebuiltsDownloadURL,
prebuiltsRootCertPath: options.caching.prebuiltsRootCertPath,
useGitLFS: options.resolver.useGitLFS,
pruneDependencies: self.options.resolver.pruneDependencies,
traitConfiguration: traitConfiguration
),
Expand Down
43 changes: 42 additions & 1 deletion Sources/SourceControl/GitRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,13 @@ private struct GitShellHelper {
public struct GitRepositoryProvider: RepositoryProvider, Cancellable {
private let cancellator: Cancellator
private let git: GitShellHelper
private let useGitLFS: Bool

private var repositoryCache = ThreadSafeKeyValueStore<String, Repository>()

public init() {
public init(useGitLFS: Bool = false) {
self.useGitLFS = useGitLFS

// helper to cancel outstanding processes
self.cancellator = Cancellator(observabilityScope: .none)
// helper to abstract shelling out to git
Expand Down Expand Up @@ -201,6 +204,44 @@ public struct GitRepositoryProvider: RepositoryProvider, Cancellable {
["--mirror"],
progress: progressHandler
)

if self.useGitLFS {
// Fetches LFS files if the repository is using Git LFS.
try self.fetchGitLFSFiles(repository: repository, at: path)
}
}

private func fetchGitLFSFiles(
repository: RepositorySpecifier,
at path: Basics.AbsolutePath,
progress: FetchProgress.Handler? = nil
) throws {
// Check to see if the .gitattributes file contains the configuration for git-lfs.
guard let output = try? self.callGit(
["-C", path.pathString, "--no-pager", "show", "HEAD:.gitattributes"],
repository: repository
), output.contains("=lfs") else {
return
}

do {
try self.callGit(
["-C", path.pathString, "lfs", "fetch", "--all"],
repository: repository,
failureMessage: "Failed to fetch Git LFS files for \(repository.location)",
progress: progress
)
} catch let error as GitCloneError {
if error.interpolationDescription.contains("'lfs' is not a git command") {
// throw an error with a more friendly message indicating that the user needs to install git-lfs
throw GitCloneError(
repository: repository,
message: "Git LFS is not installed. Please install Git LFS to use this dependency",
result: error.result
)
}
throw error
}
}

public func isValidDirectory(_ directory: Basics.AbsolutePath) throws -> Bool {
Expand Down
25 changes: 17 additions & 8 deletions Sources/SourceControl/RepositoryManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ public class RepositoryManager: Cancellable {
) async throws -> FetchDetails {
var cacheUsed = false
var cacheUpdated = false
var fetchedFromProvider = false

// utility to update progress
func updateFetchProgress(progress: FetchProgress) -> Void {
Expand Down Expand Up @@ -327,6 +328,7 @@ public class RepositoryManager: Cancellable {
}
cacheUsed = true
} else {
fetchedFromProvider = true
try self.provider.fetch(repository: handle.repository, to: cachedRepositoryPath, progressHandler: updateFetchProgress(progress:))
}
cacheUpdated = true
Expand Down Expand Up @@ -355,14 +357,21 @@ public class RepositoryManager: Cancellable {
try self.provider.copy(from: cachedRepositoryPath, to: repositoryPath)
} else {
cacheUsed = false
// Fetch without populating the cache in the case of an error.
observabilityScope.emit(
warning: "skipping cache due to an error",
underlyingError: error
)
// it is possible that we already created the directory from failed attempts, so clear leftover data if present.
try? self.fileSystem.removeFileTree(repositoryPath)
try self.provider.fetch(repository: handle.repository, to: repositoryPath, progressHandler: updateFetchProgress(progress:))

if fetchedFromProvider {
// The error was produced from the fetch, don't try and fetch
// again just propagate the error.
throw error
} else {
// Fetch without populating the cache in the case of an error.
observabilityScope.emit(
warning: "skipping cache due to an error",
underlyingError: error
)
// it is possible that we already created the directory from failed attempts, so clear leftover data if present.
try? self.fileSystem.removeFileTree(repositoryPath)
try self.provider.fetch(repository: handle.repository, to: repositoryPath, progressHandler: updateFetchProgress(progress:))
}
}
}
} else {
Expand Down
5 changes: 5 additions & 0 deletions Sources/Workspace/Workspace+Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,9 @@ public struct WorkspaceConfiguration {
/// The trait configuration for the root.
public var traitConfiguration: TraitConfiguration

/// Whether to fetch using git-lfs on dependencies that use it.
public var useGitLFS: Bool

public init(
skipDependenciesUpdates: Bool,
prefetchBasedOnResolvedFile: Bool,
Expand All @@ -820,6 +823,7 @@ public struct WorkspaceConfiguration {
usePrebuilts: Bool,
prebuiltsDownloadURL: String?,
prebuiltsRootCertPath: String?,
useGitLFS: Bool,
pruneDependencies: Bool,
traitConfiguration: TraitConfiguration
) {
Expand All @@ -838,6 +842,7 @@ public struct WorkspaceConfiguration {
self.usePrebuilts = usePrebuilts
self.prebuiltsDownloadURL = prebuiltsDownloadURL
self.prebuiltsRootCertPath = prebuiltsRootCertPath
self.useGitLFS = useGitLFS
self.pruneDependencies = pruneDependencies
self.traitConfiguration = traitConfiguration
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Workspace/Workspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ public class Workspace {
let dependencyMapper = customDependencyMapper ?? DefaultDependencyMapper(identityResolver: identityResolver)
let checksumAlgorithm = customChecksumAlgorithm ?? SHA256()

let repositoryProvider = customRepositoryProvider ?? GitRepositoryProvider()
let repositoryProvider = customRepositoryProvider ?? GitRepositoryProvider(useGitLFS: configuration.useGitLFS)
let repositoryManager = customRepositoryManager ?? RepositoryManager(
fileSystem: fileSystem,
path: location.repositoriesDirectory,
Expand Down