diff --git a/Sources/CoreCommands/Options.swift b/Sources/CoreCommands/Options.swift index 9dddd1f9c58..700037d0740 100644 --- a/Sources/CoreCommands/Options.swift +++ b/Sources/CoreCommands/Options.swift @@ -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 diff --git a/Sources/CoreCommands/SwiftCommandState.swift b/Sources/CoreCommands/SwiftCommandState.swift index f30c5103afa..01601a5017d 100644 --- a/Sources/CoreCommands/SwiftCommandState.swift +++ b/Sources/CoreCommands/SwiftCommandState.swift @@ -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 ), diff --git a/Sources/SourceControl/GitRepository.swift b/Sources/SourceControl/GitRepository.swift index fab1e9ab28f..b56ce845e76 100644 --- a/Sources/SourceControl/GitRepository.swift +++ b/Sources/SourceControl/GitRepository.swift @@ -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() - 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 @@ -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 { diff --git a/Sources/SourceControl/RepositoryManager.swift b/Sources/SourceControl/RepositoryManager.swift index 13388a752e8..80f76f1a7aa 100644 --- a/Sources/SourceControl/RepositoryManager.swift +++ b/Sources/SourceControl/RepositoryManager.swift @@ -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 { @@ -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 @@ -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 { diff --git a/Sources/Workspace/Workspace+Configuration.swift b/Sources/Workspace/Workspace+Configuration.swift index 9fecb0a0888..bd6532160d5 100644 --- a/Sources/Workspace/Workspace+Configuration.swift +++ b/Sources/Workspace/Workspace+Configuration.swift @@ -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, @@ -820,6 +823,7 @@ public struct WorkspaceConfiguration { usePrebuilts: Bool, prebuiltsDownloadURL: String?, prebuiltsRootCertPath: String?, + useGitLFS: Bool, pruneDependencies: Bool, traitConfiguration: TraitConfiguration ) { @@ -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 } diff --git a/Sources/Workspace/Workspace.swift b/Sources/Workspace/Workspace.swift index c8e6c315a33..392601e575c 100644 --- a/Sources/Workspace/Workspace.swift +++ b/Sources/Workspace/Workspace.swift @@ -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,