From 631dc6367f96bae553cccbf16eb456493fd92da3 Mon Sep 17 00:00:00 2001 From: Nanashi Li Date: Sun, 7 Jul 2024 18:13:31 +0200 Subject: [PATCH] Some bug fixes and improvements --- .../Base/Commands/Branch.swift | 56 ++++- .../Base/Commands/Checkout.swift | 6 +- .../Base/Commands/GitLog.swift | 226 +++++++++++++++++- .../Base/Commands/Interpret-Trailers.swift | 2 +- .../Version-Control/Base/Commands/Pull.swift | 48 ++++ .../Base/Commands/Status.swift | 182 +++++++++++++- .../Version-Control/Base/Commands/Tag.swift | 4 +- .../Version-Control/Base/Core/GitShell.swift | 9 +- .../Core/Parsers/GitDelimiterParser.swift | 1 - .../Models/Commands/Status/StatusResult.swift | 24 +- .../Status/WorkingDirectoryStatus.swift | 2 +- .../Base/Models/CommitIdentity.swift | 2 +- .../Base/Models/GitBranch.swift | 8 +- .../Base/Models/GitCommit.swift | 2 +- .../Services/API/GitHub/GitHubAPI.swift | 1 - .../Utils/Extensions/String.swift | 1 - .../Utils/Helpers/GitAuthor.swift | 11 +- 17 files changed, 544 insertions(+), 41 deletions(-) diff --git a/Sources/Version-Control/Base/Commands/Branch.swift b/Sources/Version-Control/Base/Commands/Branch.swift index ae24ceb3..8321a07a 100644 --- a/Sources/Version-Control/Base/Commands/Branch.swift +++ b/Sources/Version-Control/Base/Commands/Branch.swift @@ -312,12 +312,35 @@ public struct Branch { // swiftlint:disable:this type_body_length /// Creates a new branch in the specified directory. /// + /// This function creates a new branch in the specified Git repository directory. It allows + /// for an optional starting point for the new branch and an option to prevent tracking the + /// new branch. + /// /// - Parameters: /// - directoryURL: The URL of the directory where the Git repository is located. /// - name: A string representing the name of the new branch. /// - startPoint: An optional string representing the starting point for the new branch. /// - noTrack: A boolean indicating whether to track the branch. Defaults to false. /// - Throws: An error if the shell command fails. + /// + /// - Example: + /// ```swift + /// do { + /// let directoryURL = URL(fileURLWithPath: "/path/to/repository") + /// let branchName = "new-feature-branch" + /// let startPoint = "main" + /// let noTrack = true + /// try createBranch(directoryURL: directoryURL, name: branchName, startPoint: startPoint, noTrack: noTrack) + /// print("Branch created successfully.") + /// } catch { + /// print("Failed to create branch: \(error)") + /// } + /// ``` + /// + /// - Note: + /// If `noTrack` is set to `true`, the new branch will not track the remote branch from + /// which it was created. This can be useful when branching directly from a remote branch + /// to avoid automatically pushing to the remote branch's upstream. public func createBranch(directoryURL: URL, name: String, startPoint: String?, @@ -382,7 +405,7 @@ public struct Branch { // swiftlint:disable:this type_body_length /// Prepare and execute the Git command to delete the local branch using a ShellClient. try GitShell().git(args: args, path: directoryURL, - name: "deleteLocalBranch") + name: #function) // Return true to indicate that the branch deletion was attempted. return true @@ -390,15 +413,40 @@ public struct Branch { // swiftlint:disable:this type_body_length /// Deletes a remote branch in the specified directory. /// + /// This function deletes a remote branch in the specified Git repository directory. It uses the `git push` + /// command with a colon (`:`) in front of the branch name to delete the branch on the remote repository. + /// + /// If the deletion fails due to an authentication error or if the branch has already been deleted on the + /// remote, the function attempts to delete the local reference to the remote branch. + /// /// - Parameters: /// - directoryURL: The URL of the directory where the Git repository is located. /// - remoteName: A string representing the name of the remote repository. /// - remoteBranchName: A string representing the name of the branch to delete. /// - Throws: An error if the shell command fails. + /// + /// - Example: + /// ```swift + /// do { + /// let directoryURL = URL(fileURLWithPath: "/path/to/repository") + /// let remoteName = "origin" + /// let remoteBranchName = "feature-branch" + /// try deleteRemoteBranch(directoryURL: directoryURL, remoteName: remoteName, remoteBranchName: remoteBranchName) + /// print("Remote branch deleted successfully.") + /// } catch { + /// print("Failed to delete remote branch: \(error)") + /// } + /// ``` + /// + /// - Note: + /// Ensure that you have the necessary permissions to delete branches on the remote repository. If the + /// user is not authenticated or lacks the required permissions, the push operation will fail, and the + /// caller must handle this error appropriately. public func deleteRemoteBranch(directoryURL: URL, remoteName: String, - remoteBranchName: String) throws { + remoteBranchName: String) throws -> Bool { let args = [ + gitNetworkArguments.joined(), "push", remoteName, ":\(remoteBranchName)" @@ -408,7 +456,7 @@ public struct Branch { // swiftlint:disable:this type_body_length // Let this propagate and leave it to the caller to handle let result = try GitShell().git(args: args, path: directoryURL, - name: "deleteRemoteBranch", + name: #function, options: IGitExecutionOptions( expectedErrors: Set([GitError.BranchDeletionFailed]) )) @@ -421,6 +469,8 @@ public struct Branch { // swiftlint:disable:this type_body_length let ref = "refs/remotes/\(remoteName)/\(remoteBranchName)" try UpdateRef().deleteRef(directoryURL: directoryURL, ref: ref, reason: nil) } + + return true } /// Finds all branches that point at a specific commitish in the given directory. diff --git a/Sources/Version-Control/Base/Commands/Checkout.swift b/Sources/Version-Control/Base/Commands/Checkout.swift index 3f92deee..e1b45858 100644 --- a/Sources/Version-Control/Base/Commands/Checkout.swift +++ b/Sources/Version-Control/Base/Commands/Checkout.swift @@ -16,7 +16,8 @@ public struct GitCheckout { public typealias ProgressCallback = (CheckoutProgress) -> Void public func getCheckoutArgs(progressCallback: ProgressCallback?) -> [String] { - var args = gitNetworkArguments + // var args = gitNetworkArguments + var args: [String] = [] if let callback = progressCallback { args += ["checkout", "--progress"] @@ -68,7 +69,6 @@ public struct GitCheckout { public func getCheckoutOpts( // swiftlint:disable:this function_parameter_count directoryURL: URL, - account: IGitAccount?, title: String, target: String, progressCallback: ProgressCallback?, @@ -141,7 +141,6 @@ public struct GitCheckout { ) throws -> Bool { let opts = try getCheckoutOpts( directoryURL: directoryURL, - account: account, title: "Checking out branch \(branch.name)", target: branch.name, progressCallback: progressCallback, @@ -165,7 +164,6 @@ public struct GitCheckout { progressCallback: ProgressCallback?) async throws -> Bool { let opts = try getCheckoutOpts( directoryURL: directoryURL, - account: account, title: "Checking out Commit", target: shortenSHA(commit.sha), progressCallback: progressCallback, diff --git a/Sources/Version-Control/Base/Commands/GitLog.swift b/Sources/Version-Control/Base/Commands/GitLog.swift index 745e68c7..6367f3d3 100644 --- a/Sources/Version-Control/Base/Commands/GitLog.swift +++ b/Sources/Version-Control/Base/Commands/GitLog.swift @@ -9,9 +9,23 @@ import Foundation public enum CommitDate: String { + case none case lastDay = "Last 24 Hours" case lastSevenDays = "Last 7 Days" case lastThirtyDays = "Last 30 Days" + + public var gitArgs: [String] { + switch self { + case .lastDay: + return ["--since=\"24 hours ago\""] + case .lastSevenDays: + return ["--since=\"7 days ago\""] + case .lastThirtyDays: + return ["--since=\"30 days ago\""] + case .none: + return [] + } + } } public struct GitLog { @@ -22,8 +36,39 @@ public struct GitLog { // https://github.com/git/git/blob/v2.37.3/cache.h#L62-L69 let subModuleFileMode = "160000" + /// Map the submodule status based on file modes and the raw Git status. + /// + /// This function determines the submodule status based on the source and destination file modes + /// and the raw Git status string. It returns a `SubmoduleStatus` object indicating whether the + /// submodule commit has changed, has modified changes, or has untracked changes. + /// + /// - Parameters: + /// - status: The raw Git status string. + /// - srcMode: The source file mode. + /// - dstMode: The destination file mode. + /// + /// - Returns: A `SubmoduleStatus` object if the conditions for a submodule status are met, otherwise `nil`. + /// + /// - Example: + /// ```swift + /// let status = "M" + /// let srcMode = "160000" // submodule file mode + /// let dstMode = "160000" // submodule file mode + /// let submoduleStatus = mapSubmoduleStatusFileModes(status: status, srcMode: srcMode, dstMode: dstMode) + /// if let submoduleStatus = submoduleStatus { + /// print("Submodule commit changed: \(submoduleStatus.commitChanged)") + /// print("Submodule has modified changes: \(submoduleStatus.modifiedChanges)") + /// print("Submodule has untracked changes: \(submoduleStatus.untrackedChanges)") + /// } else { + /// print("No submodule status changes.") + /// } + /// ``` + /// + /// - Note: + /// This function checks if the file modes indicate a submodule and then determines the submodule status \ + /// based on the provided raw Git status string. func mapSubmoduleStatusFileModes(status: String, srcMode: String, dstMode: String) -> SubmoduleStatus? { - let subModuleFileMode = subModuleFileMode // Define subModuleFileMode here + let subModuleFileMode = subModuleFileMode if srcMode == subModuleFileMode && dstMode == subModuleFileMode && status == "M" { return SubmoduleStatus(commitChanged: true, modifiedChanges: false, untrackedChanges: false) @@ -34,6 +79,33 @@ public struct GitLog { return nil } + /// Map the raw Git status to an `AppFileStatus` object. + /// + /// This function converts a raw Git status string to an `AppFileStatus` object, \ + /// which includes information about the type of change (e.g., modified, new, deleted, etc.) and \ + /// submodule status. It also handles renames and copies with the appropriate old path if provided. + /// + /// - Parameters: + /// - rawStatus: The raw Git status string. + /// - oldPath: The optional old path of the file if it has been renamed or copied. + /// - srcMode: The source mode (file mode) of the file. + /// - dstMode: The destination mode (file mode) of the file. + /// + /// - Returns: An `AppFileStatus` object representing the file's status. + /// + /// - Example: + /// ```swift + /// let rawStatus = "R100" + /// let oldPath = "old/path/to/file.txt" + /// let srcMode = "100644" + /// let dstMode = "100644" + /// let status = mapStatus(rawStatus: rawStatus, oldPath: oldPath, srcMode: srcMode, dstMode: dstMode) + /// print("File Status: \(status.kind), Old Path: \(status.oldPath ?? "N/A")") + /// ``` + /// + /// - Note: + /// This function uses regular expressions to detect rename and copy status codes \ + /// and handles submodule statuses appropriately. func mapStatus(rawStatus: String, oldPath: String?, srcMode: String, @@ -79,12 +151,61 @@ public struct GitLog { } } + /// Determine if the given status indicates a copy or rename operation. + /// + /// - Parameter status: The `AppFileStatus` object to check. + /// + /// - Returns: `true` if the status indicates a copy or rename operation; otherwise, `false`. func isCopyOrRename(status: AppFileStatus) -> Bool { return status.kind == .copied || status.kind == .renamed } + /// Retrieve a list of commits from a Git repository. + /// + /// This function retrieves a list of commits from a specified Git repository directory. + /// It can optionally filter commits by a file path within the repository, + /// a revision range, and can limit or skip a specified number of commits. + /// The retrieved commits are parsed and returned as an array of `Commit` objects. + /// + /// - Parameters: + /// - directoryURL: The URL of the directory containing the Git repository. + /// - fileURL: An optional string representing the file path within the repository to filter commits by. + /// - revisionRange: An optional string specifying a revision range (e.g., "HEAD~5..HEAD"). + /// - limit: An optional integer specifying the maximum number of commits to retrieve. + /// - skip: An optional integer specifying the number of commits to skip. + /// - additionalArgs: An optional array of additional arguments to pass to the Git command. + /// + /// - Throws: An error of type `GitError` if the Git command fails. + /// + /// - Returns: An array of `Commit` objects representing the retrieved commits. + /// + /// - Example: + /// ```swift + /// let directoryURL = URL(fileURLWithPath: "/path/to/repository") + /// let fileURL = "path/to/file.txt" + /// + /// do { + /// let commits = try getCommits(directoryURL: directoryURL, + /// fileURL: fileURL, + /// revisionRange: "HEAD~5..HEAD", + /// limit: 10, + /// skip: 0) + /// for commit in commits { + /// print("Commit SHA: \(commit.sha)") + /// print("Commit Summary: \(commit.summary)") + /// } + /// } catch { + /// print("Failed to retrieve commits: \(error)") + /// } + /// ``` + /// + /// - Note: + /// This function uses the `git log` command to retrieve commit information. \ + /// The `fileURL` parameter can be used to filter commits that affect a specific file. \ + /// If no `fileURL` is provided, the function retrieves commits for the entire repository. public func getCommits( // swiftlint:disable:this function_body_length directoryURL: URL, + fileURL: String = "", revisionRange: String?, limit: Int?, skip: Int?, @@ -126,6 +247,10 @@ public struct GitLog { args += additionalArgs args.append("--") + if !fileURL.isEmpty { + args.append(fileURL) + } + let result = try GitShell().git(args: args, path: directoryURL, name: #function) @@ -162,6 +287,40 @@ public struct GitLog { } } + /// Retrieve the list of changed files for a specific commit in a Git repository. + /// + /// This function retrieves the list of changed files for a specific commit SHA in a given Git repository directory. + /// It uses Git's log command with options to detect renames and copies, and returns the changeset data. + /// + /// - Parameters: + /// - directoryURL: The URL of the directory containing the Git repository. + /// - sha: The commit SHA for which to retrieve the list of changed files. + /// + /// - Throws: An error of type `GitError` if the Git command fails. + /// + /// - Returns: An `IChangesetData` object containing the list of changed files, lines added, and lines deleted. + /// + /// - Example: + /// ```swift + /// let directoryURL = URL(fileURLWithPath: "/path/to/repository") + /// let commitSHA = "abcdef1234567890abcdef1234567890abcdef12" + /// + /// Task { + /// do { + /// let changesetData = try await getChangedFiles(directoryURL: directoryURL, + /// sha: commitSHA) + /// print("Files changed: \(changesetData.files)") + /// print("Lines added: \(changesetData.linesAdded)") + /// print("Lines deleted: \(changesetData.linesDeleted)") + /// } catch { + /// print("Failed to retrieve changed files: \(error)") + /// } + /// } + /// ``` + /// + /// - Note: + /// This function uses asynchronous processing to run the Git command and parse the results. + /// It detects renames and copies by using the `-M` and `-C` options in the Git log command. func getChangedFiles(directoryURL: URL, sha: String) async throws -> IChangesetData { // Opt-in for rename detection (-M) and copies detection (-C) @@ -273,6 +432,38 @@ public struct GitLog { linesDeleted: linesDeleted) } + /// Retrieve a specific commit from a Git repository by its reference. + /// + /// This function retrieves a specific commit from a Git repository directory using the provided commit reference. \ + /// It fetches the commit details and returns a `Commit` object if found. + /// + /// - Parameters: + /// - directoryURL: The URL of the directory containing the Git repository. + /// - ref: The commit reference (e.g., commit SHA, branch name, tag) to retrieve. + /// + /// - Throws: An error of type `GitError` if the Git command fails. + /// + /// - Returns: A `Commit` object representing the specified commit if found, otherwise `nil`. + /// + /// - Example: + /// ```swift + /// let directoryURL = URL(fileURLWithPath: "/path/to/repository") + /// let commitRef = "abcdef1234567890abcdef1234567890abcdef12" + /// + /// do { + /// if let commit = try getCommit(directoryURL: directoryURL, ref: commitRef) { + /// print("Commit SHA: \(commit.sha)") + /// print("Commit Summary: \(commit.summary)") + /// } else { + /// print("Commit not found.") + /// } + /// } catch { + /// print("Failed to retrieve commit: \(error)") + /// } + /// ``` + /// + /// - Note: + /// This function uses the `getCommits` function to fetch the commit details and returns the first commit found. func getCommit(directoryURL: URL, ref: String) throws -> Commit? { let commits = try getCommits(directoryURL: directoryURL, @@ -286,6 +477,39 @@ public struct GitLog { return commits[0] } + /// Check if merge commits exist after a specified commit reference in a Git repository. + /// + /// This function checks if there are any merge commits after a specified commit reference in the given Git repository directory. \ + /// If a commit reference is provided, it checks for merge commits in the revision range from that commit to `HEAD`. \ + /// If no commit reference is provided, it checks for merge commits in the entire repository history. + /// + /// - Parameters: + /// - directoryURL: The URL of the directory containing the Git repository. + /// - commitRef: An optional string representing the commit reference to start checking from. + /// + /// - Throws: An error of type `GitError` if the Git command fails. + /// + /// - Returns: `true` if there are merge commits after the specified commit reference, otherwise `false`. + /// + /// - Example: + /// ```swift + /// let directoryURL = URL(fileURLWithPath: "/path/to/repository") + /// let commitRef = "abcdef1234567890abcdef1234567890abcdef12" + /// + /// do { + /// let hasMergeCommits = try doMergeCommitsExistAfterCommit(directoryURL: directoryURL, commitRef: commitRef) + /// if hasMergeCommits { + /// print("There are merge commits after the specified commit.") + /// } else { + /// print("There are no merge commits after the specified commit.") + /// } + /// } catch { + /// print("Failed to check for merge commits: \(error)") + /// } + /// ``` + /// + /// - Note: + /// This function uses the `git log` command with the `--merges` option to filter for merge commits. func doMergeCommitsExistAfterCommit(directoryURL: URL, commitRef: String?) throws -> Bool { let commitRevRange: String? diff --git a/Sources/Version-Control/Base/Commands/Interpret-Trailers.swift b/Sources/Version-Control/Base/Commands/Interpret-Trailers.swift index 405a299d..4cff3e7c 100644 --- a/Sources/Version-Control/Base/Commands/Interpret-Trailers.swift +++ b/Sources/Version-Control/Base/Commands/Interpret-Trailers.swift @@ -15,7 +15,7 @@ public protocol ITrailer { var value: String { get } } -public class Trailer: Codable, ITrailer { +public struct Trailer: Codable, ITrailer, Hashable { public var token: String = "" public var value: String = "" diff --git a/Sources/Version-Control/Base/Commands/Pull.swift b/Sources/Version-Control/Base/Commands/Pull.swift index 346509cc..81e290e8 100644 --- a/Sources/Version-Control/Base/Commands/Pull.swift +++ b/Sources/Version-Control/Base/Commands/Pull.swift @@ -18,6 +18,28 @@ public struct GitPull { ProgressStep(title: "Checking out files", weight: 0.15) ] + /// Generate arguments for the Git pull command. + /// + /// This function generates the arguments needed for the Git pull command, including handling + /// divergent branch arguments, recurse submodules, and progress options. + /// + /// - Parameters: + /// - directoryURL: The URL of the directory containing the Git repository. + /// - remote: The name of the remote repository to pull from. + /// - account: An optional `IGitAccount` object for authentication. + /// - progressCallback: An optional callback function to handle progress updates. + /// + /// - Returns: An array of strings representing the arguments for the Git pull command. + /// + /// - Throws: An error if the arguments cannot be generated. + /// + /// - Example: + /// ```swift + /// let directoryURL = URL(fileURLWithPath: "/path/to/repository") + /// let remote = "origin" + /// let args = try getPullArgs(directoryURL: directoryURL, remote: remote, account: nil, progressCallback: nil) + /// print("Pull args: \(args)") + /// ``` func getPullArgs(directoryURL: URL, remote: String, account: IGitAccount?, @@ -37,6 +59,32 @@ public struct GitPull { return args } + /// Perform a Git pull operation. + /// + /// This function performs a Git pull operation for the specified remote repository. It supports progress + /// updates through a callback function. + /// + /// - Parameters: + /// - directoryURL: The URL of the directory containing the Git repository. + /// - account: An optional `IGitAccount` object for authentication. + /// - remote: An `IRemote` object representing the remote repository to pull from. + /// - progressCallback: An optional callback function to handle progress updates. + /// + /// - Throws: An error if the pull operation fails. + /// + /// - Example: + /// ```swift + /// let directoryURL = URL(fileURLWithPath: "/path/to/repository") + /// let remote = IRemote(name: "origin") + /// + /// do { + /// try pull(directoryURL: directoryURL, account: nil, remote: remote, progressCallback: { progress in + /// print("Pull progress: \(progress.value)% - \(progress.description)") + /// }) + /// } catch { + /// print("Failed to pull: \(error)") + /// } + /// ``` func pull(directoryURL: URL, account: IGitAccount?, remote: IRemote, diff --git a/Sources/Version-Control/Base/Commands/Status.swift b/Sources/Version-Control/Base/Commands/Status.swift index 9763ef47..d220ba1b 100644 --- a/Sources/Version-Control/Base/Commands/Status.swift +++ b/Sources/Version-Control/Base/Commands/Status.swift @@ -1,6 +1,6 @@ // -// File.swift -// +// GitStatus.swift +// // // Created by Nanashi Li on 2023/11/20. // @@ -14,6 +14,24 @@ public struct GitStatus { let MaxStatusBufferSize = 20_000_000 // 20MB in decima let conflictStatusCodes = ["DD", "AU", "UD", "UA", "DU", "AA", "UU"] + /// Parse the conflicted state of a file entry. + /// + /// This function determines the conflicted state of a file entry based on the provided details. + /// + /// - Parameters: + /// - entry: The unmerged entry representing the file conflict. + /// - path: The file path. + /// - conflictDetails: The details of the conflicts in the repository. + /// + /// - Returns: A `ConflictedFileStatus` object representing the conflicted state of the file. + /// + /// - Example: + /// ```swift + /// let conflictEntry = TextConflictEntry(details: .init(action: .BothModified)) + /// let conflictDetails = ConflictFilesDetails(conflictCountsByPath: ["file.txt": 3], binaryFilePaths: []) + /// let status = parseConflictedState(entry: conflictEntry, path: "file.txt", conflictDetails: conflictDetails) + /// print("Conflict status: \(status)") + /// ``` func parseConflictedState( entry: UnmergedEntry, path: String, @@ -36,6 +54,24 @@ public struct GitStatus { submoduleStatus: nil) } + /// Convert a file entry to an application-specific status. + /// + /// This function converts a file entry from the Git status to an application-specific status. + /// + /// - Parameters: + /// - path: The file path. + /// - entry: The file entry from the Git status. + /// - conflictDetails: The details of the conflicts in the repository. + /// - oldPath: The old path of the file if it has been renamed or copied. + /// + /// - Returns: An `AppFileStatus` object representing the status of the file. + /// + /// - Example: + /// ```swift + /// let entry = OrdinaryEntry(type: .added, submoduleStatus: nil) + /// let status = convertToAppStatus(path: "file.txt", entry: entry, conflictDetails: ConflictFilesDetails(), oldPath: nil) + /// print("App status: \(status)") + /// ``` func convertToAppStatus( path: String, entry: FileEntry, @@ -63,7 +99,34 @@ public struct GitStatus { } } - func getStatus(directoryURL: URL) async throws -> StatusResult? { // swiftlint:disable:this function_body_length + /// Retrieve the status of the working directory in a Git repository. + /// + /// This function retrieves the status of the working directory in the given Git repository directory. + /// + /// - Parameter directoryURL: The URL of the directory containing the Git repository. + /// + /// - Throws: An error of type `GitError` if the Git command fails. + /// + /// - Returns: A `StatusResult` object representing the status of the working directory. + /// + /// - Example: + /// ```swift + /// let directoryURL = URL(fileURLWithPath: "/path/to/repository") + /// + /// Task { + /// do { + /// if let statusResult = try await getStatus(directoryURL: directoryURL) { + /// print("Current branch: \(statusResult.currentBranch)") + /// print("Merge head found: \(statusResult.mergeHeadFound)") + /// } else { + /// print("Not a Git repository.") + /// } + /// } catch { + /// print("Failed to get status: \(error)") + /// } + /// } + /// ``` + public func getStatus(directoryURL: URL) async throws -> StatusResult? { // swiftlint:disable:this function_body_length let args = [ "--no-optional-locks", "status", @@ -132,6 +195,27 @@ public struct GitStatus { ) } + /// Build the status map of working directory changes. + /// + /// This function builds the status map of working directory changes based on the provided entries. + /// + /// - Parameters: + /// - files: The existing map of working directory changes. + /// - entry: The status entry from the Git status. + /// - conflictDetails: The details of the conflicts in the repository. + /// + /// - Returns: An updated map of working directory changes. + /// + /// - Example: + /// ```swift + /// let entries = [/* array of IStatusEntry */] + /// let files = [String: WorkingDirectoryFileChange]() + /// let conflictDetails = ConflictFilesDetails() + /// for entry in entries { + /// files = buildStatusMap(files: files, entry: entry, conflictDetails: conflictDetails) + /// } + /// print("Status map: \(files)") + /// ``` func buildStatusMap( files: [String: WorkingDirectoryFileChange], entry: IStatusEntry, @@ -169,6 +253,25 @@ public struct GitStatus { return files } + /// Parse the status headers from the Git status output. + /// + /// This function parses the status headers from the Git status output and updates the status header data. + /// + /// - Parameters: + /// - results: The existing status header data. + /// - header: The status header entry from the Git status output. + /// + /// - Returns: An updated `StatusHeadersData` object. + /// + /// - Example: + /// ```swift + /// let headers = [/* array of IStatusHeader */] + /// var statusHeadersData = StatusHeadersData() + /// for header in headers { + /// statusHeadersData = parseStatusHeader(results: statusHeadersData, header: header) + /// } + /// print("Status headers data: \(statusHeadersData)") + /// ``` func parseStatusHeader(results: StatusHeadersData, header: IStatusHeader) -> StatusHeadersData { var currentBranch = results.currentBranch @@ -214,18 +317,66 @@ public struct GitStatus { ) } + /// Get the details of merge conflicts in the repository. + /// + /// This function retrieves the details of merge conflicts in the given Git repository directory. + /// + /// - Parameter directoryURL: The URL of the directory containing the Git repository. + /// + /// - Throws: An error of type `GitError` if the Git command fails. + /// + /// - Returns: A `ConflictFilesDetails` object containing the details of the merge conflicts. + /// + /// - Example: + /// ```swift + /// let directoryURL = URL(fileURLWithPath: "/path/to/repository") + /// let conflictDetails = try getMergeConflictDetails(directoryURL: directoryURL) + /// print("Merge conflict details: \(conflictDetails)") + /// ``` func getMergeConflictDetails(directoryURL: URL) throws -> ConflictFilesDetails { let conflictCountsByPath = try DiffCheck().getFilesWithConflictMarkers(directoryURL: directoryURL) let binaryFilePaths = try GitDiff().getBinaryPaths(directoryURL: directoryURL, ref: "MERGE_HEAD") return ConflictFilesDetails(conflictCountsByPath: conflictCountsByPath, binaryFilePaths: binaryFilePaths) } + /// Get the details of rebase conflicts in the repository. + /// + /// This function retrieves the details of rebase conflicts in the given Git repository directory. + /// + /// - Parameter directoryURL: The URL of the directory containing the Git repository. + /// + /// - Throws: An error of type `GitError` if the Git command fails. + /// + /// - Returns: A `ConflictFilesDetails` object containing the details of the rebase conflicts. + /// + /// - Example: + /// ```swift + /// let directoryURL = URL(fileURLWithPath: "/path/to/repository") + /// let conflictDetails = try getRebaseConflictDetails(directoryURL: directoryURL) + /// print("Rebase conflict details: \(conflictDetails)") + /// ``` func getRebaseConflictDetails(directoryURL: URL) throws -> ConflictFilesDetails { let conflictCountsByPath = try DiffCheck().getFilesWithConflictMarkers(directoryURL: directoryURL) let binaryFilePaths = try GitDiff().getBinaryPaths(directoryURL: directoryURL, ref: "REBASE_HEAD") return ConflictFilesDetails(conflictCountsByPath: conflictCountsByPath, binaryFilePaths: binaryFilePaths) } + /// Get the details of working directory conflicts in the repository. + /// + /// This function retrieves the details of working directory conflicts in the given Git repository directory. + /// + /// - Parameter directoryURL: The URL of the directory containing the Git repository. + /// + /// - Throws: An error of type `GitError` if the Git command fails. + /// + /// - Returns: A `ConflictFilesDetails` object containing the details of the working directory conflicts. + /// + /// - Example: + /// ```swift + /// let directoryURL = URL(fileURLWithPath: "/path/to/repository") + /// let conflictDetails = try getWorkingDirectoryConflictDetails(directoryURL: directoryURL) + /// print("Working directory conflict details: \(conflictDetails)") + /// ``` func getWorkingDirectoryConflictDetails(directoryURL: URL) throws -> ConflictFilesDetails { let conflictCountsByPath = try DiffCheck().getFilesWithConflictMarkers(directoryURL: directoryURL) var binaryFilePaths: [String] = [] @@ -240,6 +391,31 @@ public struct GitStatus { return ConflictFilesDetails(conflictCountsByPath: conflictCountsByPath, binaryFilePaths: binaryFilePaths) } + /// Get the details of conflicts in the repository. + /// + /// This function retrieves the details of conflicts in the given Git repository directory based on the current state. + /// + /// - Parameters: + /// - directoryURL: The URL of the directory containing the Git repository. + /// - mergeHeadFound: A boolean indicating if a merge head is found. + /// - lookForStashConflicts: A boolean indicating if stash conflicts should be looked for. + /// - rebaseInternalState: The internal state of a rebase operation. + /// + /// - Throws: An error of type `GitError` if the Git command fails. + /// + /// - Returns: A `ConflictFilesDetails` object containing the details of the conflicts. + /// + /// - Example: + /// ```swift + /// let directoryURL = URL(fileURLWithPath: "/path/to/repository") + /// let mergeHeadFound = true + /// let rebaseState: RebaseInternalState? = nil + /// let conflictDetails = try getConflictDetails(directoryURL: directoryURL, + /// mergeHeadFound: mergeHeadFound, + /// lookForStashConflicts: false, + /// rebaseInternalState: rebaseState) + /// print("Conflict details: \(conflictDetails)") + /// ``` func getConflictDetails(directoryURL: URL, mergeHeadFound: Bool, lookForStashConflicts: Bool, diff --git a/Sources/Version-Control/Base/Commands/Tag.swift b/Sources/Version-Control/Base/Commands/Tag.swift index b8812b30..6595cae0 100644 --- a/Sources/Version-Control/Base/Commands/Tag.swift +++ b/Sources/Version-Control/Base/Commands/Tag.swift @@ -148,7 +148,7 @@ public struct Tag { /// - Important: /// This function is asynchronous and must be called from within an asynchronous context \ /// (e.g., an `async` function). - func getAllTags(directoryURL: URL) throws -> [String: String] { + public func getAllTags(directoryURL: URL) throws -> [String: String] { let args = ["show-ref", "--tags", "-d"] let tags = try GitShell().git(args: args, @@ -174,6 +174,8 @@ public struct Tag { return (tagName, commitSha) } + print(tagsArray) + return Dictionary(uniqueKeysWithValues: tagsArray) } diff --git a/Sources/Version-Control/Base/Core/GitShell.swift b/Sources/Version-Control/Base/Core/GitShell.swift index 0cf08ccd..86c79028 100644 --- a/Sources/Version-Control/Base/Core/GitShell.swift +++ b/Sources/Version-Control/Base/Core/GitShell.swift @@ -44,7 +44,13 @@ public struct GitShell { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/env") process.arguments = ["git"] + args - process.currentDirectoryURL = path + + // Determine the appropriate current directory for the process + if path.hasDirectoryPath { + process.currentDirectoryURL = path + } else { + process.currentDirectoryURL = path.deletingLastPathComponent() + } let stdoutPipe = Pipe() let stderrPipe = Pipe() @@ -84,7 +90,6 @@ public struct GitShell { } let commandLineURL = "/usr/bin/env git " + args.joined(separator: " ") - print("Command Line URL: \(commandLineURL)") let timeout: TimeInterval = 30 // Adjust this value as needed let timeoutDate = Date(timeIntervalSinceNow: timeout) diff --git a/Sources/Version-Control/Base/Core/Parsers/GitDelimiterParser.swift b/Sources/Version-Control/Base/Core/Parsers/GitDelimiterParser.swift index c7e44707..d4e7871a 100644 --- a/Sources/Version-Control/Base/Core/Parsers/GitDelimiterParser.swift +++ b/Sources/Version-Control/Base/Core/Parsers/GitDelimiterParser.swift @@ -98,7 +98,6 @@ struct GitDelimiterParser { consumed += 1 if consumed % keys.count == 0 { - print(entry!) entries.append(entry!) entry = nil } diff --git a/Sources/Version-Control/Base/Models/Commands/Status/StatusResult.swift b/Sources/Version-Control/Base/Models/Commands/Status/StatusResult.swift index 702821f9..e191297c 100644 --- a/Sources/Version-Control/Base/Models/Commands/Status/StatusResult.swift +++ b/Sources/Version-Control/Base/Models/Commands/Status/StatusResult.swift @@ -7,37 +7,37 @@ import Foundation -struct StatusResult { +public struct StatusResult { /// The name of the current branch. - let currentBranch: String? + public let currentBranch: String? /// The name of the current upstream branch. - let currentUpstreamBranch: String? + public let currentUpstreamBranch: String? /// The SHA of the tip commit of the current branch. - let currentTip: String? + public let currentTip: String? /// Information on how many commits ahead and behind the currentBranch is compared to the currentUpstreamBranch. - let branchAheadBehind: IAheadBehind? + public let branchAheadBehind: IAheadBehind? /// True if the repository exists at the given location. - let exists: Bool + public let exists: Bool /// True if the repository is in a conflicted state. - let mergeHeadFound: Bool + public let mergeHeadFound: Bool /// True if a merge --squash operation is started. - let squashMsgFound: Bool + public let squashMsgFound: Bool /// Details about the rebase operation, if found. - let rebaseInternalState: RebaseInternalState? + public let rebaseInternalState: RebaseInternalState? /// True if the repository is in a cherry-picking state. - let isCherryPickingHeadFound: Bool + public let isCherryPickingHeadFound: Bool /// The absolute path to the repository's working directory. - let workingDirectory: WorkingDirectoryStatus + public let workingDirectory: WorkingDirectoryStatus /// Whether conflicting files are present in the repository. - let doConflictedFilesExist: Bool + public let doConflictedFilesExist: Bool } diff --git a/Sources/Version-Control/Base/Models/Commands/Status/WorkingDirectoryStatus.swift b/Sources/Version-Control/Base/Models/Commands/Status/WorkingDirectoryStatus.swift index 0bfb7aa6..c0335ffc 100644 --- a/Sources/Version-Control/Base/Models/Commands/Status/WorkingDirectoryStatus.swift +++ b/Sources/Version-Control/Base/Models/Commands/Status/WorkingDirectoryStatus.swift @@ -7,7 +7,7 @@ import Foundation -struct WorkingDirectoryStatus { +public struct WorkingDirectoryStatus { let files: [WorkingDirectoryFileChange] let includeAll: Bool? diff --git a/Sources/Version-Control/Base/Models/CommitIdentity.swift b/Sources/Version-Control/Base/Models/CommitIdentity.swift index 4295f1fb..482fd1c8 100644 --- a/Sources/Version-Control/Base/Models/CommitIdentity.swift +++ b/Sources/Version-Control/Base/Models/CommitIdentity.swift @@ -11,7 +11,7 @@ import Foundation * A tuple of name, email, and date for the author or commit * info in a commit. */ -public struct CommitIdentity: Codable { +public struct CommitIdentity: Codable, Hashable { public let name: String public let email: String public let date: Date diff --git a/Sources/Version-Control/Base/Models/GitBranch.swift b/Sources/Version-Control/Base/Models/GitBranch.swift index bfbaa931..58a34f81 100644 --- a/Sources/Version-Control/Base/Models/GitBranch.swift +++ b/Sources/Version-Control/Base/Models/GitBranch.swift @@ -55,12 +55,12 @@ public struct ILocalBranch { } /// Struct to hold basic data about the latest commit on a Git branch. -public struct IBranchTip { +public struct IBranchTip: Hashable { /// The SHA (hash) of the latest commit. - let sha: String + public let sha: String /// Information about the author of the latest commit. - let author: CommitIdentity + public let author: CommitIdentity } /// Enum to represent different starting points for creating a Git branch. @@ -87,7 +87,7 @@ public enum BranchType: Int { case remote = 1 } -public struct GitBranch { +public struct GitBranch: Hashable { public let name: String public let upstream: String? public let tip: IBranchTip? diff --git a/Sources/Version-Control/Base/Models/GitCommit.swift b/Sources/Version-Control/Base/Models/GitCommit.swift index 55f4ed54..e04e06b7 100644 --- a/Sources/Version-Control/Base/Models/GitCommit.swift +++ b/Sources/Version-Control/Base/Models/GitCommit.swift @@ -59,7 +59,7 @@ public func extractCoAuthors(trailers: [Trailer]) -> [GitAuthor] { } /// A git commit. -public struct Commit: Codable, Equatable, Identifiable { +public struct Commit: Codable, Equatable, Identifiable, Hashable { public var id = UUID() /// A list of co-authors parsed from the commit message diff --git a/Sources/Version-Control/Services/API/GitHub/GitHubAPI.swift b/Sources/Version-Control/Services/API/GitHub/GitHubAPI.swift index 0cf83f0f..f3c3aa92 100644 --- a/Sources/Version-Control/Services/API/GitHub/GitHubAPI.swift +++ b/Sources/Version-Control/Services/API/GitHub/GitHubAPI.swift @@ -1457,7 +1457,6 @@ public struct GitHubAPI { // swiftlint:disable:this type_body_length if let fetchedRuleset = try? decoder.decode(IAPIOrganization.self, from: data) { completion(fetchedRuleset) } else { - print("Error: Unable to decode", String(data: data, encoding: .utf8) ?? "") completion(nil) } case .failure: diff --git a/Sources/Version-Control/Utils/Extensions/String.swift b/Sources/Version-Control/Utils/Extensions/String.swift index 1cc2b637..72e34119 100644 --- a/Sources/Version-Control/Utils/Extensions/String.swift +++ b/Sources/Version-Control/Utils/Extensions/String.swift @@ -51,7 +51,6 @@ extension String { String(text[Range($0.range, in: text)!]) } } catch let error { - print("invalid regex: \(error.localizedDescription)") return [] } } diff --git a/Sources/Version-Control/Utils/Helpers/GitAuthor.swift b/Sources/Version-Control/Utils/Helpers/GitAuthor.swift index 70f41142..64df6422 100644 --- a/Sources/Version-Control/Utils/Helpers/GitAuthor.swift +++ b/Sources/Version-Control/Utils/Helpers/GitAuthor.swift @@ -9,10 +9,9 @@ import Foundation -public class GitAuthor: Codable { - - var name: String - var email: String +public struct GitAuthor: Codable, Hashable, Equatable { + public var name: String + public var email: String public init(name: String?, email: String?) { self.name = name ?? "Unknown" @@ -28,4 +27,8 @@ public class GitAuthor: Codable { public func toString() -> String { return "\(self.name) \(self.email)" } + + public static func == (lhs: GitAuthor, rhs: GitAuthor) -> Bool { + lhs.email == rhs.email + } }