diff --git a/.github/workflows/uploadAssets.yml b/.github/workflows/uploadAssets.yml index 1e11910..0b977b0 100644 --- a/.github/workflows/uploadAssets.yml +++ b/.github/workflows/uploadAssets.yml @@ -84,6 +84,8 @@ jobs: apt install -yq swiftlang swift build -c release --enable-test-discovery --static-swift-stdlib -Xswiftc -static-executable cp .build/release/LFSPointers . + ./LFSPointers --generate-completion-script zsh > _LFSPointers + ./LFSPointers --generate-completion-script bash > LFSPointers.bash mkdir LFSPointers-$TAG-linux-arm64 cp .build/release/LFSPointers LFSPointers-$TAG-linux-arm64 tar -czf LFSPointers-${{ github.event.release.tag_name }}-linux-arm64.tar.gz LFSPointers-$TAG-linux-arm64 @@ -161,6 +163,8 @@ jobs: run: | swift build -c release --enable-test-discovery --static-swift-stdlib -Xswiftc -static-executable cp .build/release/LFSPointers . + ./LFSPointers --generate-completion-script zsh > _LFSPointers + ./LFSPointers --generate-completion-script bash > LFSPointers.bash mkdir LFSPointers-$TAG-linux-amd64 cp .build/release/LFSPointers LFSPointers-$TAG-linux-amd64 tar -czf LFSPointers-$TAG-linux-amd64.tar.gz LFSPointers-$TAG-linux-amd64 diff --git a/.swiftformat b/.swiftformat index b91664f..2adfbe4 100644 --- a/.swiftformat +++ b/.swiftformat @@ -11,12 +11,12 @@ "lineBreakBeforeControlFlowKeywords" : false, "lineBreakBeforeEachArgument" : false, "lineBreakBeforeEachGenericRequirement" : false, - "lineLength" : 100, + "lineLength" : 120, "maximumBlankLines" : 2, "prioritizeKeepingFunctionOutputTogether" : false, "respectsExistingLineBreaks" : true, "rules" : { - "AllPublicDeclarationsHaveDocumentation" : truee, + "AllPublicDeclarationsHaveDocumentation" : true, "AlwaysUseLowerCamelCase" : true, "AmbiguousTrailingClosureOverload" : true, "BeginDocumentationCommentWithOneLineSummary" : true, @@ -30,7 +30,7 @@ "NeverUseForceTry" : false, "NeverUseImplicitlyUnwrappedOptionals" : false, "NoAccessLevelOnExtensionDeclaration" : true, - "NoBlockComments" : true, + "NoBlockComments" : false, "NoCasesWithOnlyFallthrough" : true, "NoEmptyTrailingClosureParentheses" : true, "NoLabelsInCasePatterns" : true, diff --git a/Sources/LFSPointersExecutable/main.swift b/Sources/LFSPointersExecutable/main.swift index dffa7b8..d263d2e 100644 --- a/Sources/LFSPointersExecutable/main.swift +++ b/Sources/LFSPointersExecutable/main.swift @@ -1,8 +1,15 @@ -import Foundation +// +// main.swift +// +// +// Created by Jeff Lebrun on 0/00/20. +// + import ArgumentParser -import Rainbow import Files +import Foundation import LFSPointersKit +import Rainbow let jsonStructure = """ [ @@ -34,16 +41,16 @@ struct LFSPointersCommand: ParsableCommand { discussion: "JSON STRUCTURE:\n\(jsonStructure)", version: "3.0.0" ) - + @Flag(name: .shortAndLong, help: "Whether to display verbose output.") var verbose: Bool = false - + @Flag(name: .customLong("silent"), help: "Don't print to standard output or standard error.") var s: Bool = false - + @Flag(name: .shortAndLong, help: "Repeat this process in all directories.") var recursive: Bool = false - + @Flag(name: .shortAndLong, help: "Convert all files to pointers (USE WITH CAUTION!).") var all: Bool = false @@ -69,20 +76,20 @@ struct LFSPointersCommand: ParsableCommand { transform: URL.init(fileURLWithPath:) ) var backupDirectory: URL? = nil - + @Argument( help: "The directory which contains the files you want to convert to LFS pointers.", completion: .directory, transform: URL.init(fileURLWithPath:) ) var directory: URL - + @Argument( help: "A list of paths to files, relative to the current directory, that represent files to be converted. You can use your shell's regular expression support to pass in a list of files.", completion: .file() ) var files: [String] = [] - + mutating func validate() throws { // Verify the directory actually exists. guard FileManager().fileExists(atPath: directory.path) else { @@ -97,22 +104,22 @@ struct LFSPointersCommand: ParsableCommand { } } } - + func run() throws { var silent = false - + if s { silent = true } - + if json { silent = true } - + if !color { Rainbow.enabled = false } - + let printClosure: (URL, Status) -> Void = { url, status in switch status { case let .appending(pointer): @@ -122,17 +129,17 @@ struct LFSPointersCommand: ParsableCommand { } else if !silent { print("Appending pointer to file \"\(file.name)\"...") } - + case let .error(error): let file = try! File(path: url.path) - + if self.verbose && !silent { if self.color { fputs("Could not convert \"\(file.name)\" to a pointer.\n Git LFS error: \(error)\n".red, stderr) } else { fputs("Could not convert \"\(file.name)\" to a pointer.\n Git LFS error: \(error)\n", stderr) } - + } else if !silent { if self.color { fputs("Could not convert \"\(file.name)\" to a pointer.".red, stderr) @@ -141,10 +148,10 @@ struct LFSPointersCommand: ParsableCommand { } } break - + case .generating: let file = try! File(path: url.path) - + if !silent && self.verbose { print("Converting \"\(file.name)\" to pointer...\n") } else if !silent { @@ -152,11 +159,11 @@ struct LFSPointersCommand: ParsableCommand { } case let .regexDoesntMatch(regex): let file = try! File(path: url.path) - + if !silent && self.verbose { print("File name \"\(file.name)\" does not match regular expression \"\(regex.pattern)\", continuing...") } - + case let .writing(pointer): let file = try! File(path: url.path) if self.verbose && !silent { @@ -166,7 +173,7 @@ struct LFSPointersCommand: ParsableCommand { } } } - + do { if all { @@ -180,13 +187,13 @@ struct LFSPointersCommand: ParsableCommand { Foundation.exit(0) } } - + if let bd = backupDirectory { do { if !silent { print("Copying files to backup directory...") } - + // Copy the specified directory into the backup directory. try Folder(path: directory.path).copy(to: Folder(path: bd.path)) } catch { @@ -204,11 +211,11 @@ struct LFSPointersCommand: ParsableCommand { ) } } - + Foundation.exit(4) } } - + if all { let pointers = try LFSPointer.pointers( forDirectory: directory, @@ -216,10 +223,10 @@ struct LFSPointersCommand: ParsableCommand { recursive: recursive, statusClosure: printClosure ) - + if !json { pointers.forEach({ p in - + do { try p.write( toFile: URL(fileURLWithPath: p.filePath), @@ -240,7 +247,7 @@ struct LFSPointersCommand: ParsableCommand { fputs("Unable to overwrite file \"\(p.filename)\". Error: \(error)", stderr) } } - + }) } else { do { @@ -261,10 +268,10 @@ struct LFSPointersCommand: ParsableCommand { recursive: recursive, statusClosure: printClosure ) - + if !json { pointers.forEach({ p in - + do { try p.write( toFile: URL(fileURLWithPath: p.filePath), @@ -286,7 +293,7 @@ struct LFSPointersCommand: ParsableCommand { fputs("Unable to overwrite file \"\(p.filename)\". Error: \(error)", stderr) } } - + }) } else { do { @@ -299,7 +306,7 @@ struct LFSPointersCommand: ParsableCommand { } } - + } catch let error { if !silent { @@ -310,10 +317,10 @@ struct LFSPointersCommand: ParsableCommand { } } - + Foundation.exit(2) } - + if !silent { if self.color { diff --git a/Sources/LFSPointersKit/Enums.swift b/Sources/LFSPointersKit/Enums.swift index 4638f52..0210bde 100644 --- a/Sources/LFSPointersKit/Enums.swift +++ b/Sources/LFSPointersKit/Enums.swift @@ -1,6 +1,6 @@ // // Enums.swift -// +// // // Created by Jeff Lebrun on 4/16/20. // @@ -9,18 +9,17 @@ import Foundation /// The search types to use when filtering files. public enum SearchTypes { - + /// Searches for all files that match any of the filenames in the array. case fileNames([URL]) - + /// Searches for all files whose name matches the regular expression. case regex(NSRegularExpression) - + /// Searches for all files. case all } - public enum Status { case writing(LFSPointer), appending(LFSPointer), diff --git a/Sources/LFSPointersKit/Extensions.swift b/Sources/LFSPointersKit/Extensions.swift index 0d5b797..edc6ccf 100644 --- a/Sources/LFSPointersKit/Extensions.swift +++ b/Sources/LFSPointersKit/Extensions.swift @@ -1,6 +1,6 @@ // // Extensions.swift -// +// // // Created by Jeff Lebrun on 4/14/20. // @@ -9,6 +9,7 @@ import Foundation public extension NSRegularExpression { /// Checks if this regular expression matches the supplied `String`. + /// /// - Returns: `true` if the `String` matches, otherwise, `false`. func matches(_ string: String) -> Bool { firstMatch(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count)) != nil diff --git a/Sources/LFSPointersKit/Pointers.swift b/Sources/LFSPointersKit/Pointers.swift index f4843a0..7cdf651 100644 --- a/Sources/LFSPointersKit/Pointers.swift +++ b/Sources/LFSPointersKit/Pointers.swift @@ -1,13 +1,13 @@ // // Pointers.swift -// +// // // Created by Jeff Lebrun on 4/14/20. // -import Foundation -import Files import Crypto +import Files +import Foundation /// Represents a Git LFS pointer for a file. /// @@ -30,12 +30,14 @@ import Crypto /// /// ``` public struct LFSPointer: Codable, Equatable, Hashable { - /// The version of the pointer. Example: "https://git-lfs.github.com/spec/v1". + /// The version of the pointer. + /// + /// Example: "https://git-lfs.github.com/spec/v1". public let version: String - + /// An SHA 256 hash for the pointer. public let oid: String - + /// The size of the converted file. public let size: Int @@ -44,29 +46,29 @@ public struct LFSPointer: Codable, Equatable, Hashable { /// The full path of the file. public let filePath: String - + /// String representation of this pointer. public var stringRep: String { "version \(self.version)\noid sha256:\(self.oid)\nsize \(self.size)" } - + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - + try container.encode(self.version, forKey: .version) try container.encode(self.oid, forKey: .oid) try container.encode(self.size, forKey: .size) try container.encode(self.filename, forKey: .filename) try container.encode(self.filePath, forKey: .filePath) } - + /// Initializes `self` from a file. /// - Parameters: /// - path: The path to the file. /// - Throws: `LocationError` if the file path is invalid. public init(fromFile path: URL) throws { let file = try File(path: path.path) - + self.version = "https://git-lfs.github.com/spec/v1" let handle = try FileHandle(forReadingFrom: file.url) @@ -84,7 +86,7 @@ public struct LFSPointer: Codable, Equatable, Hashable { } self.oid = String(hexEncoding: Data(hasher.finalize())) - + try handle.close() let fp = fopen(file.path, "r") @@ -98,8 +100,9 @@ public struct LFSPointer: Codable, Equatable, Hashable { self.filename = file.name self.filePath = file.path } - + /// Iterates over all files in a directory (excluding hidden files), and generates a LFS pointer for each one. + /// /// - Parameters: /// - directory: The directory to iterate over. /// - recursive: Whether to include subdirectories when iterating. @@ -109,6 +112,7 @@ public struct LFSPointer: Codable, Equatable, Hashable { /// - statusClosure: Use this closure to determine the status of this function. It will be passed the `URL` of the file or folder being operated on, as well as an enum representing the status of this function. /// - Throws: `LocationError` if the directory path is invalid. /// - Returns: An array of `LFSPointer`. + /// public static func pointers( forDirectory directory: URL, searchType type: SearchTypes, @@ -116,9 +120,9 @@ public struct LFSPointer: Codable, Equatable, Hashable { statusClosure status: ((URL, Status) -> Void)? = nil ) throws -> [LFSPointer] { var pointers: [LFSPointer] = [] - + let folder = try Folder(path: directory.path) - + if recursive { switch type { case .fileNames(let fileNames): @@ -131,37 +135,37 @@ public struct LFSPointer: Codable, Equatable, Hashable { } let folder = try Folder(path: directory.path) - + let folderNames = folder.files.recursive.names() - + for f in files { if folderNames.contains(f.name) { if status != nil { status!(f.url, .generating) } - + do { pointers.append(try self.init(fromFile: f.url)) } catch let error { if status != nil { status!(f.url, .error(error)) } - + throw error } } } - + case .regex(let regex): try folder.files.recursive.forEach({ file in if regex.matches(file.name) { - + if status != nil { status!(file.url, .generating) } - + do { pointers.append(try self.init(fromFile: file.url)) } catch let error { if status != nil { status!(file.url, .error(error)) } - + throw error } - + } else { if status != nil { status!(file.url, .regexDoesntMatch(regex)) } } @@ -169,17 +173,17 @@ public struct LFSPointer: Codable, Equatable, Hashable { case .all: try folder.files.recursive.forEach({ file in if status != nil { status!(file.url, .generating) } - + do { pointers.append(try self.init(fromFile: file.url)) } catch let error { if status != nil { status!(file.url, .error(error)) } - + throw error } }) } - + } else { switch type { case .fileNames(let fileNames): @@ -194,28 +198,28 @@ public struct LFSPointer: Codable, Equatable, Hashable { for f in files { if folder.containsFile(named: f.name) { if status != nil { status!(f.url, .generating) } - + do { pointers.append(try self.init(fromFile: f.url)) } catch let error { if status != nil { status!(f.url, .error(error)) } - + throw error } } } case .regex(let regex): - + for file in folder.files { if regex.matches(file.name) { - + do { if status != nil { status!(file.url, .generating) } - + pointers.append(try self.init(fromFile: file.url)) } catch let error { if status != nil { status!(file.url, .error(error)) } - + throw error } } else { @@ -226,21 +230,22 @@ public struct LFSPointer: Codable, Equatable, Hashable { for file in folder.files { do { if status != nil { status!(file.url, .generating) } - + pointers.append(try self.init(fromFile: file.url)) } catch let error { if status != nil { status!(file.url, .error(error)) } - + throw error } } } } - + return pointers } - + /// Write `self` (`LFSPointer`) to a file. + /// /// - Parameters: /// - file: The file to write or append to. /// - shouldAppend: If the file should be appended to. @@ -248,26 +253,27 @@ public struct LFSPointer: Codable, Equatable, Hashable { /// - printOutput: Whether output should be printed. /// - printVerboseOutput: Whether verbose output should be printed. /// - Throws: `LocationError` if the file path is invalid, or `WriteError` if the file could not be written. + /// public func write( toFile file: URL, withNewline: Bool = false, shouldAppend: Bool = false, statusClosure status: ((URL, Status) -> Void)? = nil ) throws { - + let file = try File(path: file.path) - + if shouldAppend { if status != nil { status!(file.url, .appending(self)) } - + try file.append("version \(self.version)\noid sha256:\(self.oid)\nsize \(self.size)\(withNewline ? "\n" : "")", encoding: .utf8) } else { if status != nil { status!(file.url, .writing(self)) } - + try file.write("version \(self.version)\noid sha256:\(self.oid)\nsize \(self.size)\(withNewline ? "\n" : "")", encoding: .utf8) } } - + enum CodingKeys: String, CodingKey { case version, oid, size, filename, filePath } @@ -279,12 +285,13 @@ extension LFSPointer: CustomDebugStringConvertible { } } -public extension Array where Self.Element == LFSPointer { +extension Array where Self.Element == LFSPointer { /// Converts and `Array` of `LFSPointer`s to `JSON`. + /// /// - Parameter jsonFormat: The format of the generated `JSON`. /// - Throws: `EncodingError` when generating `JSON` fails. /// - Returns: `Data` containing the generated `JSON`. - func toJSON(inFormat jsonFormat: JSONEncoder.OutputFormatting = .init()) throws -> Data { + public func toJSON(inFormat jsonFormat: JSONEncoder.OutputFormatting = .init()) throws -> Data { let encoder = JSONEncoder() encoder.outputFormatting = jsonFormat return try encoder.encode(self) diff --git a/package.yaml b/package.yaml index 18ea4f6..834e7be 100644 --- a/package.yaml +++ b/package.yaml @@ -5,4 +5,8 @@ meta: files: "/usr/bin/LFSPointers": file: LFSPointers - mode: "0755" \ No newline at end of file + mode: "0755" + "/etc/bash_completion.d/LFSPointers.bash": + file: LFSPointers.bash + "/usr/share/zsh/site-functions/_LFSPointers": + file: _LFSPointers \ No newline at end of file