├── .gitattributes ├── .github └── workflows │ ├── build-documentation.yaml │ ├── build.yml │ └── lint.yml ├── .gitignore ├── .swiftlint.yml ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── Version-Control │ ├── Base │ ├── Actions │ │ └── GitHub │ │ │ └── GitHubActions.swift │ ├── Commands │ │ ├── Add.swift │ │ ├── Apply.swift │ │ ├── Branch.swift │ │ ├── Check.swift │ │ ├── Checkout-Index.swift │ │ ├── Checkout.swift │ │ ├── Cherry-Pick.swift │ │ ├── Clone.swift │ │ ├── Commit.swift │ │ ├── Config.swift │ │ ├── Description.swift │ │ ├── Diff-Check.swift │ │ ├── Diff-Index.swift │ │ ├── Diff.swift │ │ ├── Format-Patch.swift │ │ ├── GitIgnore.swift │ │ ├── GitLog.swift │ │ ├── Init.swift │ │ ├── Interpret-Trailers.swift │ │ ├── LFS.swift │ │ ├── Merge.swift │ │ ├── Pull.swift │ │ ├── Push.swift │ │ ├── RM.swift │ │ ├── Rebase.swift │ │ ├── Reflog.swift │ │ ├── Refs.swift │ │ ├── Remote.swift │ │ ├── Reset.swift │ │ ├── Rev-List.swift │ │ ├── Rev-Parse.swift │ │ ├── Revert.swift │ │ ├── Squash.swift │ │ ├── Stage.swift │ │ ├── Stash.swift │ │ ├── Status.swift │ │ ├── Submodule.swift │ │ ├── Tag.swift │ │ ├── Update-Index.swift │ │ └── Update-Ref.swift │ ├── Core │ │ ├── Blob │ │ │ └── Blob.swift │ │ ├── Core.swift │ │ ├── GitProcess.swift │ │ ├── GitShell.swift │ │ ├── IExecutionOptions.swift │ │ ├── IGitExecutionOptions.swift │ │ ├── IGitResult.swift │ │ ├── IResult.swift │ │ ├── Parsers │ │ │ ├── DiffParser.swift │ │ │ ├── GitCherryPickParser.swift │ │ │ ├── GitDelimiterParser.swift │ │ │ ├── GitErrorParser.swift │ │ │ ├── GitRebaseParser.swift │ │ │ ├── GitStatusParser.swift │ │ │ └── PatchFormatterParser.swift │ │ ├── ProcessError.swift │ │ └── Progress │ │ │ ├── From-Process.swift │ │ │ ├── GitProgress.swift │ │ │ └── LFSProgress.swift │ └── Models │ │ ├── CloneOptions.swift │ │ ├── Commands │ │ ├── Diff │ │ │ ├── Diff Component Types │ │ │ │ └── DiffImage.swift │ │ │ ├── Diff Types │ │ │ │ ├── IBinaryDiff.swift │ │ │ │ ├── IImageDiff.swift │ │ │ │ ├── ILargeTextDiff.swift │ │ │ │ ├── ISubmoduleDiff‎.swift │ │ │ │ ├── ITextDiff.swift │ │ │ │ └── IUnrenderableDiff.swift │ │ │ ├── DiffType.swift │ │ │ ├── IDiff.swift │ │ │ ├── IDiffTypes.swift │ │ │ └── ITextDiffData.swift │ │ ├── Log │ │ │ └── IChangesetData.swift │ │ ├── Rebase │ │ │ ├── ComputedAction.swift │ │ │ ├── GitRebaseSnapshot.swift │ │ │ ├── RebaseInternalState.swift │ │ │ ├── RebaseProgressOptions.swift │ │ │ └── RebaseResult.swift │ │ └── Status │ │ │ ├── ConflictFilesDetails.swift │ │ │ ├── IStatusEntry.swift │ │ │ ├── IStatusHeader.swift │ │ │ ├── StatusHeadersData.swift │ │ │ ├── StatusResult.swift │ │ │ └── WorkingDirectoryStatus.swift │ │ ├── CommitHistory.swift │ │ ├── CommitIdentity.swift │ │ ├── Diff │ │ ├── Diff-Data.swift │ │ ├── Diff-Line.swift │ │ ├── Diff-Selection.swift │ │ ├── Helper │ │ │ └── Diff-Helper.swift │ │ └── Raw-Diff.swift │ │ ├── Files │ │ ├── AppFileStatus.swift │ │ ├── CommittedFileChange.swift │ │ ├── FileChange.swift │ │ └── WorkingDirectoryFileChange.swift │ │ ├── GitBranch.swift │ │ ├── GitCommit.swift │ │ ├── GitFileItem.swift │ │ ├── GitType.swift │ │ ├── IGitAccount.swift │ │ ├── IProgress.swift │ │ ├── IRemote.swift │ │ ├── ManualConflictResolution.swift │ │ └── Stash-Entry.swift │ ├── Errors │ ├── GitError.swift │ ├── IndexError.swift │ ├── NetworkingError.swift │ └── ShellErrors.swift │ ├── Services │ ├── API │ │ ├── BitBucket │ │ │ ├── BitBucketAPI.swift │ │ │ └── Interfaces │ │ │ │ └── Repo │ │ │ │ ├── IBitBucketAPIPullRequest.swift │ │ │ │ ├── IBitbucketComment.swift │ │ │ │ └── IBitbucketRendered.swift │ │ ├── GitHub │ │ │ ├── AuthorizationResponse.swift │ │ │ ├── AuthorizationResponseKind.swift │ │ │ ├── GitHubAPI.swift │ │ │ ├── GithubNetworkingConstants.swift │ │ │ ├── Interfaces │ │ │ │ ├── Account │ │ │ │ │ ├── IAPIEmail.swift │ │ │ │ │ ├── IAPIFullIdentity.swift │ │ │ │ │ ├── IAPIIdentity.swift │ │ │ │ │ └── IAPIOrganization.swift │ │ │ │ └── Repo │ │ │ │ │ ├── Branch │ │ │ │ │ └── IAPIBranch.swift │ │ │ │ │ ├── IAPIFullRepository.swift │ │ │ │ │ ├── IAPIMentionableResponse.swift │ │ │ │ │ ├── IAPIRepository.swift │ │ │ │ │ ├── IAPIRepositoryCloneInfo.swift │ │ │ │ │ ├── IAPIRepositoryPermissions.swift │ │ │ │ │ ├── Issues │ │ │ │ │ └── IAPIIssue.swift │ │ │ │ │ ├── Pull Request │ │ │ │ │ ├── IAPIComment.swift │ │ │ │ │ ├── IAPIPullRequest.swift │ │ │ │ │ ├── IAPIPullRequestRef.swift │ │ │ │ │ └── IAPIPullRequestReview.swift │ │ │ │ │ ├── Push │ │ │ │ │ └── IAPIPushControl.swift │ │ │ │ │ ├── Ruleset │ │ │ │ │ ├── IAPIRepoRule.swift │ │ │ │ │ ├── IAPIRepoRuleMetadataParameters.swift │ │ │ │ │ ├── IAPIRepoRuleset.swift │ │ │ │ │ ├── IAPISlimRepoRuleset.swift │ │ │ │ │ └── IRawAPIRepoRule.swift │ │ │ │ │ └── Workflow │ │ │ │ │ ├── IAPICheckSuite.swift │ │ │ │ │ ├── IAPIRefCheckRun.swift │ │ │ │ │ ├── IAPIRefCheckRunApp.swift │ │ │ │ │ ├── IAPIRefCheckRunCheckSuite.swift │ │ │ │ │ ├── IAPIRefCheckRunOutput.swift │ │ │ │ │ ├── IAPIRefCheckRuns.swift │ │ │ │ │ ├── IAPIRefStatus.swift │ │ │ │ │ ├── IAPIRefStatusItem.swift │ │ │ │ │ ├── IAPIWorkflowJob.swift │ │ │ │ │ ├── IAPIWorkflowJobStep.swift │ │ │ │ │ ├── IAPIWorkflowJobs.swift │ │ │ │ │ ├── IAPIWorkflowRun.swift │ │ │ │ │ └── IAPIWorkflowRuns.swift │ │ │ └── Model │ │ │ │ ├── Account │ │ │ │ └── GithubAccount.swift │ │ │ │ └── Repo │ │ │ │ ├── Issues │ │ │ │ └── APIIssueState.swift │ │ │ │ ├── Pull Request │ │ │ │ └── APIPullRequestReviewState.swift │ │ │ │ ├── Ruleset │ │ │ │ ├── APIRepoRuleMetadataOperator.swift │ │ │ │ └── APIRepoRuleType.swift │ │ │ │ └── Workflow │ │ │ │ ├── APICheckConclusion.swift │ │ │ │ ├── APICheckStatus.swift │ │ │ │ └── APIRefState.swift │ │ ├── Gitlab │ │ │ └── GitlabAPI.swift │ │ └── Global │ │ │ └── Gravatar.swift │ ├── Models │ │ └── Github │ │ │ ├── Actions │ │ │ ├── Jobs │ │ │ │ ├── Job.swift │ │ │ │ ├── JobSteps.swift │ │ │ │ └── Jobs.swift │ │ │ └── Workflow │ │ │ │ ├── Workflow.swift │ │ │ │ ├── WorkflowRun.swift │ │ │ │ ├── WorkflowRuns.swift │ │ │ │ └── Workflows.swift │ │ │ └── Auth │ │ │ ├── 2FA.swift │ │ │ └── GitHubEmail.swift │ └── Networking │ │ ├── AuroraNetworking.swift │ │ ├── AuroraNetworkingConstants.swift │ │ ├── AuroraNetworkingDebug.swift │ │ ├── HTTPErrors.swift │ │ └── HTTPMethod.swift │ ├── Utils │ ├── BranchUtil.swift │ ├── CommandError.swift │ ├── Extensions │ │ ├── Date.swift │ │ ├── FileManager.swift │ │ └── String.swift │ ├── FileUtils.swift │ ├── Helpers │ │ ├── DefaultBranch.swift │ │ ├── GitAuthor.swift │ │ ├── MediaDiff.swift │ │ ├── Regex.swift │ │ └── RemoveRemotePrefix.swift │ ├── LiveShellClient.swift │ └── ShellClient.swift │ └── Version_Control.swift └── Tests └── Version-Control-Test └── Services ├── API └── GitHub │ └── Mock Data │ ├── GitHubAccountResponse.json │ ├── ProtectedBranchesResponse.json │ └── PushControlResponse.json └── Auth └── GitHub └── GitHubEmailTest.swift /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/build-documentation.yaml: -------------------------------------------------------------------------------- 1 | name: build-documentation 2 | 3 | on: 4 | # Run on push to main branch 5 | push: 6 | branches: 7 | - main 8 | 9 | # Dispatch if triggered using Github (website) 10 | workflow_dispatch: 11 | 12 | jobs: 13 | Build-documentation: 14 | runs-on: macos-latest 15 | steps: 16 | - name: Build documentation 17 | uses: 0xWDG/build-documentation@main 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | jobs: 8 | build: 9 | runs-on: macos-12 10 | timeout-minutes: 10 # If a build exceeds 10 mins, it probably isn't ever going to complete 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: MacOS Version 14 | run: sw_vers 15 | - name: Toolchain version 16 | run: swift -version 17 | - name: Build 18 | run: swift build 19 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: SwiftLint 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | jobs: 8 | lint: 9 | runs-on: macos-12 10 | timeout-minutes: 10 # If a build exceeds 10 mins, it probably isn't ever going to complete 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: MacOS Version 14 | run: sw_vers 15 | - name: Toolchain version 16 | run: swift -version 17 | - name: SwiftLint (version) 18 | run: swiftlint version 19 | - name: SwiftLint (GH Actions) 20 | run: swiftlint --reporter github-actions-logging --strict 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | .idea/ 11 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # Swiftlint configuration file. 2 | # Part of AuroraEditor. 3 | # Please do not remove optional rules, feel free to add some if needed. 4 | 5 | # Disabled rule, reason. 6 | disabled_rules: 7 | - todo # New project, we have a lot of // TODO: 8 | - missing_docs # We miss a lot of docs right now 9 | - identifier_name # This should be fixed in a future version 10 | - force_try # This should be fixed in a future version 11 | # Exclude triggering type names. 12 | type_name: 13 | excluded: 14 | - ID 15 | 16 | # Exclude triggering identifier names. 17 | identifier_name: 18 | excluded: 19 | - id 20 | - vc 21 | - to 22 | # (short) File extensions: 23 | - c 24 | - m 25 | - h 26 | - js 27 | - md 28 | - py 29 | - go 30 | - ts 31 | - txt 32 | - sh 33 | - pf 34 | - r 35 | - q 36 | - tp 37 | - xl 38 | - hy 39 | - d 40 | - cs 41 | 42 | # cyclomatic_complexity (ignore case) 43 | cyclomatic_complexity: 44 | ignores_case_statements: true 45 | 46 | # Opt in rules, we want it more stricter. 47 | opt_in_rules: 48 | - empty_count 49 | - closure_spacing 50 | - contains_over_first_not_nil 51 | - missing_docs 52 | - modifier_order 53 | - convenience_type 54 | - pattern_matching_keywords 55 | - identical_operands 56 | - empty_string 57 | 58 | # Custom configuration for nesting, this needs to be removed at some point. 59 | nesting: 60 | type_level: 61 | warning: 2 # warning if you nest 2 level deep instead of 1 62 | error: 3 # error if you nest 3 level deep instead of 1 63 | 64 | # Custom rules 65 | custom_rules: 66 | # Prefer spaces over tabs. 67 | spaces_over_tabs: 68 | included: ".*\\.swift" 69 | name: "Spaces over Tabs" 70 | regex: "\t" 71 | message: "Prefer spaces for indents over tabs. See Xcode setting: 'Text Editing' -> 'Indentation'" 72 | severity: warning 73 | 74 | # @Something needs a new line 75 | at_attributes_newlining: 76 | name: "Significant attributes" 77 | message: "Significant @attributes should be on an extra line" 78 | included: ".*.swift" 79 | regex: '(@objc\([^\)]+\)|@nonobjc|@discardableResult|@propertyWrapper|@UIApplicationMain|@dynamicMemberLookup|@_cdecl\([^\)]+\))[^\n]' 80 | severity: error 81 | 82 | # forbid multiple empty lines 83 | multiple_empty_lines: 84 | included: ".*.swift" 85 | name: "Multiple Empty Lines" 86 | regex: '((?:\s*\n){3,})' 87 | message: "There are too many line breaks" 88 | severity: error 89 | 90 | # one space after a comma 91 | comma_space_rule: 92 | regex: ",[ ]{2,}" 93 | message: "Expected only one space after '," 94 | severity: error 95 | 96 | # Disable usage of // swiftlint:disable (rulename) 97 | swiftlint_file_disabling: 98 | included: ".*.swift" 99 | name: "SwiftLint File Disabling" 100 | regex: "swiftlint:disable\\s" 101 | match_kinds: comment 102 | message: "Use swiftlint:disable:next or swiftlint:disable:this" 103 | severity: error 104 | 105 | # Disable Xcode placeholders like <#Description#> 106 | no_placeholders: 107 | included: ".*.swift" 108 | name: "No Placeholders" 109 | regex: "\\<\\#([a-zA-Z]+)\\#\\>" 110 | message: "Please do not use Xcode's placeholders." 111 | severity: warning 112 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Version-Control", 8 | platforms: [.macOS(.v12)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, and make them visible to other packages. 11 | .library( 12 | name: "Version-Control", 13 | targets: ["Version-Control"]) 14 | ], 15 | dependencies: [ 16 | // Dependencies declare other packages that this package depends on. 17 | // .package(url: /* package url */, from: "1.0.0"), 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 22 | .target( 23 | name: "Version-Control", 24 | dependencies: []), 25 | .testTarget( 26 | name: "Version-Control-Test", 27 | dependencies: [ 28 | "Version-Control" 29 | ] 30 | ) 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Logo 3 |

4 | 5 | # Version Control Kit 6 | 7 | ## Overview 8 | The AuroraEditor Version Control Kit is a comprehensive tool designed to facilitate version control operations within the AuroraEditor environment. It enables actions such as committing, pulling, pushing, and fetching history for entire files or specific lines of code. This project is an extraction from the main AuroraEditor Repository and is currently under development. 9 | 10 | ## Features (Not limited to the below mentioned) 11 | - **Committing Changes**: Track and commit changes to your codebase. 12 | - **Pulling Updates**: Synchronize your local repository with changes from a remote repository. 13 | - **Pushing Changes**: Send your local commits to a remote repository. 14 | - **Fetching History**: Access the history of changes for files or individual lines of code. 15 | - **Git Services**: Allows you to make calls to GitHub, BitBucket and GitLab. (Create an issue if you want more Git providers added.) 16 | 17 | ## Installation 18 | 19 | ### Requirements 20 | - Swift 3.0 or later. 21 | - macOS Monterey or later 22 | 23 | ### Using Swift Package Manager 24 | 25 | 1. **Add Dependency**: 26 | Edit your `Package.swift` to include AuroraEditor Version Control Kit as a dependency: 27 | 28 | ```swift 29 | dependencies: [ 30 | .package(url: "https://github.com/AuroraEditor/Version-Control-Kit.git", from: "Version-0") 31 | ] 32 | ``` 33 | 34 | Else if you want the latest up to date version use the branch name: 35 | 36 | ```swift 37 | dependencies: [ 38 | .package(url: "https://github.com/AuroraEditor/Version-Control-Kit.git", .branch("main")) 39 | ] 40 | ``` 41 | ## Usage 42 | 43 | The AuroraEditor Version Control Kit's source code is in the process of being thoroughly documented. While many functions include detailed explanations, cautionary notes, and examples with their requirements, there are some areas still pending comprehensive documentation. These will be addressed and updated in due time. Users can look forward to a future How-To guide that will provide additional structured guidance on using the toolkit's capabilities. 44 | 45 | ## Socials 46 | 47 |

48 | 49 | Twitter Follow 50 | 51 | 52 | Discord 53 | 54 |

55 | 56 | ## Licenses 57 | 58 | ### Intellectual Property License 59 | - The AuroraEditor Version Control Logo is the copyright of AuroraEditor and Aurora Company. 60 | 61 | ### GNU Affero General Public License v3.0 62 | - This project is licensed under the GNU AGPL V3 License. 63 | 64 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Actions/GitHub/GitHubActions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitHubActions.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/25. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | public enum GitHubViewType: String { 12 | case tree 13 | case compare 14 | } 15 | 16 | public struct GitHubActions { 17 | 18 | internal func getBranchName( 19 | directoryURL: URL, 20 | completion: @escaping (Result) -> Void 21 | ) { 22 | Task { 23 | do { 24 | let branchName = try await Branch().getCurrentBranch(directoryURL: directoryURL) 25 | completion(.success(branchName)) 26 | } catch { 27 | completion(.failure(error)) 28 | } 29 | } 30 | } 31 | 32 | internal func getCurrentRepositoryGitHubURL(directoryURL: URL) throws -> String { 33 | let remoteUrls: [GitRemote] = try Remote().getRemotes(directoryURL: directoryURL) 34 | 35 | for remote in remoteUrls where remote.url.contains("github.com") { 36 | return remote.url 37 | } 38 | 39 | return "" 40 | } 41 | 42 | /// Open a specific branch of a GitHub repository in a web browser. 43 | /// 44 | /// This function constructs the URL for a specific branch of 45 | /// a GitHub repository based on the provided parameters and opens it in the default web browser. 46 | /// 47 | /// - Parameters: 48 | /// - viewType: The type of view to open on GitHub (e.g., code, commits, pulls). 49 | /// - directoryURL: The local directory URL of the Git repository. 50 | /// 51 | /// - Throws: An error if there's an issue with constructing the URL or opening it in the web browser. 52 | /// 53 | /// - Example: 54 | /// ```swift 55 | /// do { 56 | /// let viewType = GitHubViewType.commits 57 | /// let directoryURL = URL(fileURLWithPath: "/path/to/local/repository") 58 | /// try openBranchOnGitHub(viewType: viewType, directoryURL: directoryURL) 59 | /// } catch { 60 | /// print("Error: Unable to open the GitHub branch.") 61 | /// } 62 | public func openBranchOnGitHub(viewType: GitHubViewType, 63 | directoryURL: URL) throws { 64 | let htmlURL = try getCurrentRepositoryGitHubURL(directoryURL: directoryURL) 65 | 66 | var branchName = "" 67 | 68 | getBranchName(directoryURL: directoryURL) { result in 69 | switch result { 70 | case .success(let name): 71 | branchName = name 72 | case .failure(let error): 73 | branchName = "" 74 | } 75 | } 76 | 77 | let urlEncodedBranchName = branchName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) 78 | 79 | guard let encodedBranchName = urlEncodedBranchName else { 80 | return 81 | } 82 | 83 | let url = URL(string: "\(htmlURL)/\(viewType)/\(encodedBranchName)") 84 | 85 | NSWorkspace.shared.open(url!) 86 | } 87 | 88 | /// Open the GitHub issue creation page for the current repository in a web browser. 89 | /// 90 | /// This function constructs the URL for creating a new issue in 91 | /// the current repository on GitHub and opens it in the default web browser. 92 | /// 93 | /// - Parameter directoryURL: The local directory URL of the Git repository. 94 | /// 95 | /// - Throws: An error if there's an issue with constructing the URL or opening it in the web browser. 96 | /// 97 | /// - Example: 98 | /// ```swift 99 | /// do { 100 | /// let directoryURL = URL(fileURLWithPath: "/path/to/local/repository") 101 | /// try openIssueCreationOnGitHub(directoryURL: directoryURL) 102 | /// } catch { 103 | /// print("Error: Unable to open the GitHub issue creation page.") 104 | /// } 105 | public func openIssueCreationOnGitHub(directoryURL: URL) throws { 106 | let repositoryURL = try getCurrentRepositoryGitHubURL(directoryURL: directoryURL) 107 | 108 | let url = URL(string: "\(repositoryURL)/issues/new/choose") 109 | 110 | NSWorkspace.shared.open(url!) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Commands/Add.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Add.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/12. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // This source code is restricted for Aurora Editor usage only. 8 | // 9 | 10 | import Foundation 11 | 12 | public struct Add { 13 | /// Stages a file that has conflicts after a Git operation such as a merge or cherry-pick. 14 | /// 15 | /// This function is typically used to mark a file with conflicts as resolved by adding it to the staging area. 16 | /// After resolving the conflicts manually in the file, you would call this function to stage the file. 17 | /// 18 | /// - Parameters: 19 | /// - directoryURL: The URL of the directory where the Git repository is located. 20 | /// - file: A `WorkingDirectoryFileChange` object representing the file with conflicts to be staged. 21 | /// - Throws: An error if the `git add` command fails. 22 | func addConflictedFile(directoryURL: URL, 23 | file: WorkingDirectoryFileChange) throws { 24 | 25 | try GitShell().git(args: ["add", "--", file.path], 26 | path: directoryURL, 27 | name: #function) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Commands/Check.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Check.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/09/08. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Check { 12 | 13 | public init() {} 14 | 15 | /// Checks if a given workspace directory is a Git repository or a Git worktree. 16 | /// 17 | /// - Parameter workspaceURL: The URL of the workspace directory to be checked. 18 | /// 19 | /// - Returns: `true` if the workspace is a Git repository or worktree, `false` otherwise. 20 | /// 21 | /// - Note: This function checks the type of the workspace using `getRepositoryType`, \ 22 | /// and if it's marked as unsafe by Git, \ 23 | /// it falls back to a naive approximation by looking for the `.git` directory. 24 | /// 25 | /// - Example: 26 | /// ```swift 27 | /// let workspaceURL = URL(fileURLWithPath: "/path/to/workspace") 28 | /// 29 | /// if checkIfProjectIsRepo(workspaceURL: workspaceURL) { 30 | /// print("The workspace is a Git repository or worktree.") 31 | /// } else { 32 | /// print("The workspace is not a Git repository or worktree.") 33 | /// } 34 | /// ``` 35 | /// 36 | /// - Warning: 37 | /// Ensure that the specified `workspaceURL` exists and is a valid directory. 38 | public func checkIfProjectIsRepo(workspaceURL: URL) -> Bool { 39 | do { 40 | let type = try RevParse().getRepositoryType(directoryURL: workspaceURL) 41 | 42 | switch type { 43 | case .missing: 44 | return false 45 | case .unsafe: 46 | return FileManager().directoryExistsAtPath("\(workspaceURL)/.git") 47 | case .bare, .regular: 48 | return true 49 | } 50 | 51 | } catch { 52 | print("We couldn't verify if the current project is a Git repo!") 53 | return false 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Commands/Checkout-Index.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/10/29. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct CheckoutIndex { 11 | 12 | public init() {} 13 | 14 | public func checkoutIndex(directoryURL: URL, 15 | paths: [String]) async throws { 16 | 17 | if paths.isEmpty { 18 | return 19 | } 20 | 21 | let options: IGitExecutionOptions = IGitExecutionOptions( 22 | stdin: paths.joined(separator: "\0"), 23 | successExitCodes: Set([0, 1]) 24 | ) 25 | 26 | try GitShell().git(args: ["checkout-index", "-f", "-u", "-q", "--stdin", "-z"], 27 | path: directoryURL, 28 | name: #function, 29 | options: options) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Commands/Clone.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Clone.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/12. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // This source code is restricted for Aurora Editor usage only. 8 | // 9 | 10 | import Foundation 11 | 12 | public struct Clone { 13 | 14 | public init() {} 15 | 16 | /// Clones a repository from a given url into to the specified path. 17 | /// 18 | /// @param url - The remote repository URL to clone from 19 | /// 20 | /// @param path - The destination path for the cloned repository. If the 21 | /// path does not exist it will be created. Cloning into an 22 | /// existing directory is only allowed if the directory is 23 | /// empty. 24 | /// 25 | /// @param options - Options specific to the clone operation, see the 26 | /// documentation for CloneOptions for more details. 27 | /// 28 | /// @param progressCallback - An optional function which will be invoked 29 | /// with information about the current progress 30 | /// of the clone operation. When provided this enables 31 | /// the '--progress' command line flag for 32 | /// 'git clone'. 33 | typealias CloneProgressCallback = (CloneProgress) -> Void 34 | 35 | let steps: [ProgressStep] = [ 36 | ProgressStep(title: "remote: Compressing objects", weight: 0.1), 37 | ProgressStep(title: "Receiving objects", weight: 0.6), 38 | ProgressStep(title: "Resolving deltas", weight: 0.1), 39 | ProgressStep(title: "Checking out files", weight: 0.2) 40 | ] 41 | 42 | func clone(directoryURL: URL, 43 | path: String, 44 | options: CloneOptions, 45 | progressCallback: ((ICloneProgress) -> Void)? = nil) async throws { 46 | // let env = try await envForRemoteOperation(options.account, url) 47 | let defaultBranch = options.defaultBranch ?? (DefaultBranch().getDefaultBranch()) 48 | 49 | var args = gitNetworkArguments + ["-c", "init.defaultBranch=\(defaultBranch)", "clone", "--recursive"] 50 | var gitOptions: IGitExecutionOptions = IGitExecutionOptions() 51 | 52 | if let progress = progressCallback { 53 | args.append("--progress") 54 | 55 | let title = "Cloning into \(path)" 56 | let kind = "clone" 57 | 58 | gitOptions = try FromProcess().executionOptionsWithProgress( 59 | options: gitOptions, 60 | parser: GitProgressParser(steps: steps), 61 | progressCallback: { progressInfo in 62 | var description: String = "" 63 | 64 | if let gitProgress = progressInfo as? IGitProgress, progressInfo.kind == "progress" { 65 | description = gitProgress.details.text 66 | } else if let gitOutput = progressInfo as? IGitOutput { 67 | description = gitOutput.text 68 | } 69 | 70 | let value = progressInfo.percent 71 | 72 | progressCallback?(CloneProgress(kind: kind, value: value, title: title, description: description)) 73 | } 74 | ) 75 | 76 | progressCallback?(CloneProgress(kind: kind, value: 0, title: title)) 77 | } 78 | 79 | if let branch = options.branch { 80 | args += ["-b", branch] 81 | } 82 | 83 | args += ["--", directoryURL.relativePath.escapedWhiteSpaces(), path] 84 | 85 | try GitShell().git(args: args, 86 | path: directoryURL, 87 | name: #function, 88 | options: gitOptions) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Commands/Description.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Description.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/13. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // This source code is restricted for Aurora Editor usage only. 8 | // 9 | 10 | import Foundation 11 | 12 | private let gitDescriptionPath = ".git/description" 13 | 14 | private let defaultGitDescription = "Unnamed repository; edit this file 'description' to name the repository.\n" 15 | 16 | public struct GitDescription { 17 | 18 | public init() {} 19 | 20 | /// Get the project's description from the `.git/description` file. 21 | /// 22 | /// This function retrieves the project's description from the `.git/description` 23 | /// file within a Git repository located at the specified `directoryURL`. \ 24 | /// The description typically provides a brief overview of the project. 25 | /// 26 | /// - Parameter directoryURL: The URL of the directory containing the Git repository. 27 | /// 28 | /// - Throws: 29 | /// - An error of type `Error` if any issues occur during the description retrieval process. 30 | /// 31 | /// - Returns: 32 | /// The project's description as a string, or an empty string if the description \ 33 | /// is not found or cannot be retrieved. 34 | /// 35 | /// - Example: 36 | /// ```swift 37 | /// let directoryURL = URL(fileURLWithPath: "/path/to/repo") // Replace with the path to the Git repository 38 | /// 39 | /// do { 40 | /// let projectDescription = try getGitDescription(directoryURL: directoryURL) 41 | /// if !projectDescription.isEmpty { 42 | /// print("Project Description: \(projectDescription)") 43 | /// } else { 44 | /// print("Project description not found.") 45 | /// } 46 | /// } catch { 47 | /// print("Error retrieving project description: \(error.localizedDescription)") 48 | /// } 49 | /// ``` 50 | /// 51 | /// - Note: 52 | /// This function reads the content of the `.git/description` file in the Git repository specified by 53 | /// `directoryURL` and returns it as a string. \ 54 | /// If the description is not found or cannot be retrieved, an empty string is returned. 55 | public func getGitDescription(directoryURL: URL) throws -> String { 56 | let path = try String(contentsOf: directoryURL) + gitDescriptionPath 57 | 58 | do { 59 | let data = try String(contentsOf: URL(string: path)!) 60 | if data == defaultGitDescription { 61 | return "" 62 | } 63 | return data 64 | } catch { 65 | return "" 66 | } 67 | } 68 | 69 | /// Write a project's description to the `.git/description` file within a Git repository. 70 | /// 71 | /// This function writes the provided `description` to the `.git/description` file within 72 | /// a Git repository located at the specified `directoryURL`. \ 73 | /// The description typically provides a brief overview of the project. 74 | /// 75 | /// - Parameters: 76 | /// - directoryURL: The URL of the directory containing the Git repository. 77 | /// - description: The project's description to be written to the file. 78 | /// 79 | /// - Throws: 80 | /// - An error of type `Error` if any issues occur during the description writing process. 81 | /// 82 | /// - Example: 83 | /// ```swift 84 | /// let directoryURL = URL(fileURLWithPath: "/path/to/repo") // Replace with the path to the Git repository 85 | /// let projectDescription = "My Awesome Project" // Replace with the desired project description 86 | /// 87 | /// do { 88 | /// try writeGitDescription(directoryURL: directoryURL, description: projectDescription) 89 | /// print("Project Description has been updated.") 90 | /// } catch { 91 | /// print("Error writing project description: \(error.localizedDescription)") 92 | /// } 93 | /// ``` 94 | /// 95 | /// - Note: 96 | /// This function writes the provided `description` to the `.git/description` file 97 | /// in the Git repository specified by `directoryURL`. \ 98 | /// It does so by overwriting the existing content of the file, if any. 99 | public func writeGitDescription(directoryURL: URL, 100 | description: String) throws { 101 | let fullPath = try String(contentsOf: directoryURL) + gitDescriptionPath 102 | try description.write(toFile: fullPath, atomically: false, encoding: .utf8) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Commands/Diff-Check.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Diff-Check.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/29. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct DiffCheck { 12 | 13 | public init() {} 14 | 15 | /// Matches a line reporting a leftover conflict marker 16 | /// and captures the name of the file 17 | let pattern = "^.+:(\\d+): leftover conflict marker$" 18 | 19 | /// Returns a list of files with conflict markers present 20 | public func getFilesWithConflictMarkers(directoryURL: URL) throws -> [String: Int] { 21 | let args = ["diff", "--check"] 22 | 23 | let output = try GitShell().git(args: args, 24 | path: directoryURL, 25 | name: #function, 26 | options: IGitExecutionOptions(successExitCodes: Set([0, 2]))) 27 | 28 | let captures = Regex().getCaptures(text: output.stdout, 29 | expression: try NSRegularExpression(pattern: pattern, 30 | options: .caseInsensitive)) 31 | 32 | if captures.isEmpty { 33 | return [:] 34 | } 35 | 36 | // Flatten the list (only does one level deep) 37 | let flatCaptures = captures.flatMap { $0 } 38 | 39 | var counted: [String: Int] = [:] 40 | for val in flatCaptures { 41 | counted[val, default: 0] += 1 42 | } 43 | 44 | return counted 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Commands/Diff-Index.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Diff-Index.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/29. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Possible statuses of an entry in Git 12 | public enum IndexStatus: Int { 13 | case unknown = 0 14 | case added 15 | case copied 16 | case deleted 17 | case modified 18 | case renamed 19 | case typeChanged 20 | case unmerged 21 | } 22 | 23 | public enum NoRenameIndexStatus { 24 | case added 25 | case deleted 26 | case modified 27 | case typeChanged 28 | case unmerged 29 | case unknown 30 | } 31 | 32 | public struct DiffIndex { 33 | 34 | public init() {} 35 | 36 | public func getIndexStatus(status: String) throws -> IndexStatus { 37 | switch status.substring(0) { 38 | case "A": 39 | return IndexStatus.added 40 | case "C": 41 | return IndexStatus.copied 42 | case "D": 43 | return IndexStatus.deleted 44 | case "M": 45 | return IndexStatus.modified 46 | case "R": 47 | return IndexStatus.renamed 48 | case "T": 49 | return IndexStatus.typeChanged 50 | case "U": 51 | return IndexStatus.unmerged 52 | case "X": 53 | return IndexStatus.unknown 54 | default: 55 | throw IndexError.unknownIndex("Unknown index status: \(status)") 56 | } 57 | } 58 | 59 | public func getNoRenameIndexStatus(_ status: String) throws -> NoRenameIndexStatus { 60 | do { 61 | let parsed = try getIndexStatus(status: status) 62 | 63 | switch parsed { 64 | case .unknown: 65 | return .unknown 66 | case .added: 67 | return .added 68 | case .copied, .renamed: 69 | throw IndexError.invalidStatus("Invalid index status for no-rename index status: \(parsed.rawValue)") 70 | case .deleted: 71 | return .deleted 72 | case .modified: 73 | return .modified 74 | case .typeChanged: 75 | return .typeChanged 76 | case .unmerged: 77 | return .unmerged 78 | } 79 | } catch { 80 | throw IndexError.invalidStatus("Unknown status: \(status)") 81 | } 82 | } 83 | 84 | /// The SHA for the nil tree 85 | public let nilTreeSHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904" 86 | 87 | /// Get the status of changes in the Git index (staging area). 88 | /// 89 | /// This function retrieves the status of changes in the Git index (staging area) for all tracked files. 90 | /// 91 | /// - Parameters: 92 | /// - directoryURL: The URL of the Git repository directory where the `git diff-index` command will be executed. 93 | /// 94 | /// - Returns: A dictionary where the keys are file paths and the values are `IndexStatus` \ 95 | /// representing the status of each file in the index. 96 | /// 97 | /// - Throws: An error if there is a problem executing the `git diff-index` command or \ 98 | /// if the Git repository is not in a valid state. 99 | /// 100 | /// - SeeAlso: `git diff-index` documentation for additional options and details. 101 | public func getIndexChanges(directoryURL: URL) throws -> [String: NoRenameIndexStatus] { 102 | let args = [ 103 | "diff-index", 104 | "--cached", 105 | "name-status", 106 | "--no-renames", 107 | "-z" 108 | ] 109 | 110 | var result = try GitShell().git(args: args + ["HEAD", "--"], 111 | path: directoryURL, 112 | name: #function, 113 | options: IGitExecutionOptions(successExitCodes: Set([0, 128]))) 114 | 115 | if result.exitCode == 128 { 116 | result = try GitShell().git(args: args + [nilTreeSHA], 117 | path: directoryURL, 118 | name: #function) 119 | } 120 | 121 | let pieces = result.stdout.split(separator: "\0") 122 | 123 | var map = [String: NoRenameIndexStatus]() 124 | 125 | for number in stride(from: 0, to: pieces.count - 1, by: 2) { 126 | let statusString = String(pieces[number]) 127 | let path = String(pieces[number + 1]) 128 | let status = try getIndexStatus(status: statusString) 129 | 130 | switch status { 131 | case .unknown: 132 | map[path] = .unknown 133 | case .added: 134 | map[path] = .added 135 | case .deleted: 136 | map[path] = .deleted 137 | case .modified: 138 | map[path] = .modified 139 | case .typeChanged: 140 | map[path] = .typeChanged 141 | case .unmerged: 142 | map[path] = .unmerged 143 | default: 144 | map[path] = .unknown 145 | } 146 | } 147 | 148 | return map 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Commands/Format-Patch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Format-Patch.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/13. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // This source code is restricted for Aurora Editor usage only. 8 | // 9 | 10 | import Foundation 11 | 12 | public struct FormatPatch { 13 | 14 | public init() {} 15 | 16 | /// Creates a patch string representation of changes between two commits. 17 | /// 18 | /// This asynchronous function leverages the `GitShell` utility to run the `git format-patch` 19 | /// command, which generates a patch string for the changes between two specified revisions in a Git repository. 20 | /// 21 | /// - Parameters: 22 | /// - directoryURL: A `URL` pointing to the Git repository's directory. 23 | /// - base: A `String` representing the base commit or reference. 24 | /// - head: A `String` representing the head commit or reference. 25 | /// 26 | /// - Returns: A `String` containing the patch data. 27 | /// 28 | /// - Throws: An error if the `git` command fails or if there are issues accessing the repository. 29 | /// 30 | /// The function constructs a revision range from the base to the head parameters, then passes this along with 31 | /// other arguments to the `git` command via `GitShell`. The command specifies a unified diff with minimal 32 | /// context and directs the output to standard output instead of creating files. The function awaits the result and 33 | /// returns the standard output, which contains the patch data. 34 | /// 35 | /// This is an asynchronous function, and it must be called with `await` in an asynchronous context. 36 | /// The use of `try` indicates that the function can throw an error which must be handled by the caller. 37 | func formatPatch(directoryURL: URL, 38 | base: String, 39 | head: String) async throws -> String { 40 | let range = RevList().revRange(from: base, to: head) 41 | let output = try GitShell().git(args: ["format-patch", "--unified=1", "--minimal", "--stdout", range], 42 | path: directoryURL, 43 | name: #function) 44 | return output.stdout 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Commands/Init.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Init.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/13. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // This source code is restricted for Aurora Editor usage only. 8 | // 9 | 10 | import Foundation 11 | 12 | public struct Git { 13 | 14 | public init() {} 15 | 16 | /// Initialize a new Git repository in the specified directory. 17 | /// 18 | /// This function creates a new Git repository in the provided directory and configures the 19 | /// default branch name based on system settings. \ 20 | /// If a Git repository already exists in the specified directory, this function has no effect. 21 | /// 22 | /// - Parameters: 23 | /// - directoryURL: The URL of the directory where the Git repository should be initialized. 24 | /// 25 | /// - Throws: An error if there was an issue initializing the Git repository. 26 | /// 27 | /// - Example: 28 | /// ```swift 29 | /// do { 30 | /// try initGitRepository(directoryURL: myProjectDirectoryURL) 31 | /// print("Git repository initialized successfully.") 32 | /// } catch { 33 | /// print("Error: \(error)") 34 | /// } 35 | /// ``` 36 | /// 37 | /// - Note: If a Git repository already exists in the specified directory, 38 | /// this function will not reinitialize it and will have no effect. 39 | /// 40 | /// - Important: Make sure to call this function to initialize a new Git repository 41 | /// in a directory before performing Git operations on that directory. 42 | public func initGitRepository(directoryURL: URL) throws { 43 | try ShellClient().run( 44 | // swiftlint:disable:next line_length 45 | "cd \(directoryURL.relativePath.escapedWhiteSpaces());git -c init.defaultBranch=\(DefaultBranch().getDefaultBranch()) init" 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Commands/Push.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Push.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/01. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct PushOptions { 11 | /** 12 | * Force-push the branch without losing changes in the remote that 13 | * haven't been fetched. 14 | * 15 | * See https://git-scm.com/docs/git-push#Documentation/git-push.txt---no-force-with-lease 16 | */ 17 | let forceWithLease: Bool = false 18 | } 19 | 20 | public struct Push { 21 | 22 | public init() {} 23 | 24 | public func push( // swiftlint:disable:this function_parameter_count 25 | directoryURL: URL, 26 | remote: IRemote, 27 | localBranch: String, 28 | remoteBranch: String?, 29 | tagsToPush: [String]?, 30 | options: PushOptions, 31 | progressCallback: ((IPushProgress) -> Void)? = nil 32 | ) throws { 33 | var args = gitNetworkArguments + [ 34 | "push", 35 | remote.name, 36 | remoteBranch != nil ? "\(localBranch):\(remoteBranch!)" : localBranch 37 | ] 38 | 39 | if let tags = tagsToPush { 40 | args += tags 41 | } 42 | 43 | if remoteBranch == nil { 44 | args.append("--set-upstream") 45 | } else if options.forceWithLease { 46 | args.append("--force-with-lease") 47 | } 48 | 49 | // TODO: Add progress support 50 | 51 | let result = try GitShell().git(args: args, 52 | path: directoryURL, 53 | name: #function) 54 | 55 | if result.gitErrorDescription != nil { 56 | throw GitErrorParser(result: result, 57 | args: args) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Commands/RM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RM.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/13. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // This source code is restricted for Aurora Editor usage only. 8 | // 9 | 10 | import Foundation 11 | 12 | public struct RM { // swiftlint:disable:this type_name 13 | 14 | public init() {} 15 | 16 | /// Remove all files from the Git index. 17 | /// 18 | /// This function removes all files from the Git index (staging area) in a Git repository \ 19 | /// located at the specified `directoryURL`. \ 20 | /// The files are removed from the staging area while keeping them in the working directory. 21 | /// 22 | /// - Parameters: 23 | /// - directoryURL: The URL of the directory containing the Git repository. 24 | /// 25 | /// - Throws: 26 | /// - An error of type `Error` if any issues occur during the file removal from the index. 27 | /// 28 | /// - Example: 29 | /// ```swift 30 | /// let directoryURL = URL(fileURLWithPath: "/path/to/repo") // Replace with the path to the Git repository 31 | /// 32 | /// do { 33 | /// try unstageAllFiles(directoryURL: directoryURL) 34 | /// print("All files have been removed from the Git index.") 35 | /// } catch { 36 | /// print("Error removing files from the Git index: \(error.localizedDescription)") 37 | /// } 38 | /// ``` 39 | /// 40 | /// - Note: 41 | /// This function uses the `git rm --cached -r -f .` command to remove all files from the Git \ 42 | /// index while preserving them in the working directory. 43 | /// 44 | /// - Warning: 45 | /// Exercise caution when using this function, \ 46 | /// as it can lead to the removal of all staged changes without committing them. \ 47 | /// Make sure you understand the implications of unstaging files from the index. 48 | public func unstageAllFiles(directoryURL: URL) throws { 49 | 50 | // these flags are important: 51 | // --cached - to only remove files from the index 52 | // -r - to recursively remove files, in case files are in folders 53 | // -f - to ignore differences between working directory and index 54 | // which will block this 55 | try GitShell().git(args: ["rm", 56 | "--chached", 57 | "-r", 58 | "-f", 59 | "."], 60 | path: directoryURL, 61 | name: #function) 62 | } 63 | 64 | /// Remove a conflicted file from both the working tree and the Git index (staging area). 65 | /// 66 | /// This function removes a conflicted file specified by `file` from both the working tree and the \ 67 | /// Git index (staging area) in a Git repository located at the specified `directoryURL`. \ 68 | /// The file will be deleted from the working directory, and the removal will be staged for the next commit. 69 | /// 70 | /// - Parameters: 71 | /// - directoryURL: The URL of the directory containing the Git repository. 72 | /// - file: The `GitFileItem` representing the conflicted file to be removed. 73 | /// 74 | /// - Throws: 75 | /// - An error of type `Error` if any issues occur during the removal process. 76 | /// 77 | /// - Example: 78 | /// ```swift 79 | /// let directoryURL = URL(fileURLWithPath: "/path/to/repo") // Replace with the path to the Git repository 80 | /// let conflictedFile = GitFileItem(url: URL(fileURLWithPath: "path/to/conflicted/file"), gitStatus: .conflicted) 81 | /// 82 | /// do { 83 | /// try removeConflictedFile(directoryURL: directoryURL, file: conflictedFile) 84 | /// print("Conflicted file \(conflictedFile.url.path) has been removed.") 85 | /// } catch { 86 | /// print("Error removing conflicted file: \(error.localizedDescription)") 87 | /// } 88 | /// ``` 89 | /// 90 | /// - Note: 91 | /// This function uses the `git rm` command with the `--` flag to remove the specified conflicted file \ 92 | /// from both the working directory and the Git index. 93 | /// 94 | /// - Warning: 95 | /// Be cautious when using this function, as it permanently deletes the conflicted file \ 96 | /// from both the working directory and the Git index. 97 | public func removeConflictedFile(directoryURL: URL, 98 | file: WorkingDirectoryFileChange) throws { 99 | try GitShell().git(args: ["rm", 100 | "--", 101 | file.path], 102 | path: directoryURL, 103 | name: #function) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Commands/Revert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Revert.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/13. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // This source code is restricted for Aurora Editor usage only. 8 | // 9 | 10 | import Foundation 11 | 12 | public struct GitRevert { 13 | /// Creates a new commit that reverts the changes of a previous commit 14 | /// 15 | /// @param sha - The SHA of the commit to be reverted 16 | func revertCommit(directoryURL: URL, 17 | commit: Commit, 18 | progressCallback: ((RevertProgress) -> Void)? 19 | ) throws { 20 | var args = gitNetworkArguments + ["revert"] 21 | if commit.parentSHAs.count > 1 { 22 | args += ["-m", "1"] 23 | } 24 | 25 | args.append(commit.sha) 26 | 27 | var opts: IGitExecutionOptions? 28 | if let progressCallback = progressCallback { 29 | opts = try FromProcess().executionOptionsWithProgress( 30 | options: IGitExecutionOptions(trackLFSProgress: true), 31 | parser: GitProgressParser(steps: [ProgressStep(title: "", weight: 0)]), 32 | progressCallback: { progress in 33 | var description: String = "" 34 | var title: String = "" 35 | 36 | if let gitProgress = progress as? IGitProgress, progress.kind == "progress" { 37 | description = gitProgress.details.text 38 | title = gitProgress.details.title 39 | } else if let gitOutput = progress as? IGitOutput { 40 | description = gitOutput.text 41 | title = "" 42 | } 43 | 44 | let value = progress.percent 45 | 46 | progressCallback(RevertProgress(kind: "revert", 47 | value: value, 48 | title: title, 49 | description: description)) 50 | } 51 | ) 52 | } 53 | 54 | try GitShell().git(args: args, 55 | path: directoryURL, 56 | name: #function, 57 | options: opts) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Commands/Squash.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Squash.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/13. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // This source code is restricted for Aurora Editor usage only. 8 | // 9 | 10 | import Foundation 11 | 12 | public struct GitSquash { 13 | 14 | /// Squashes provided commits by calling interactive rebase. 15 | /// 16 | /// Goal is to replay the commits in order from oldest to newest to reduce 17 | /// conflicts with toSquash commits placed in the log at the location of the 18 | /// squashOnto commit. 19 | /// 20 | /// Example: A user's history from oldest to newest is A, B, C, D, E and they 21 | /// want to squash A and E (toSquash) onto C. Our goal: B, A-C-E, D. Thus, 22 | /// maintaining that A came before C and E came after C, placed in history at the 23 | /// the squashOnto of C. 24 | /// 25 | /// Also means if the last 2 commits in history are A, B, whether user squashes A 26 | /// onto B or B onto A. It will always perform based on log history, thus, B onto 27 | /// A. 28 | func squash( // swiftlint:disable:this function_body_length 29 | directoryURL: URL, 30 | toSquash: [Commit], 31 | squashOnto: Commit, 32 | lastRetainedCommitRef: String?, 33 | commitMessage: String, 34 | progressCallback: ((MultiCommitOperationProgress) -> Void)? = nil 35 | ) async throws -> RebaseResult { 36 | var messagePath: String? 37 | var todoPath: String? 38 | var result: RebaseResult = .error 39 | 40 | do { 41 | guard !toSquash.isEmpty else { 42 | throw NSError( 43 | domain: "com.auroraeditor.versioncontrolkit.squash", 44 | code: 1, 45 | userInfo: [NSLocalizedDescriptionKey: "No commits provided to squash."] 46 | ) 47 | } 48 | 49 | let toSquashShas = Set(toSquash.map { $0.sha }) 50 | guard !toSquashShas.contains(squashOnto.sha) else { 51 | throw NSError( 52 | domain: "com.auroraeditor.versioncontrolkit.squash", 53 | code: 2, 54 | userInfo: [ 55 | NSLocalizedDescriptionKey: "The commits to squash cannot contain the commit to squash onto." 56 | ] 57 | ) 58 | } 59 | 60 | let commits = try GitLog().getCommits( 61 | directoryURL: directoryURL, 62 | revisionRange: lastRetainedCommitRef == nil ? nil : "\(lastRetainedCommitRef!)..HEAD", 63 | limit: nil, 64 | skip: nil 65 | ) 66 | 67 | guard !commits.isEmpty else { 68 | throw NSError( 69 | domain: "com.auroraeditor.versioncontrolkit.squash", 70 | code: 3, 71 | userInfo: [NSLocalizedDescriptionKey: "Could not find commits in log for last retained commit ref."] 72 | ) 73 | } 74 | 75 | todoPath = try await FileUtils().writeToTempFile(content: "", tempFileName: "squashTodo") 76 | 77 | // Logic for building the todoPath content goes here 78 | 79 | if !commitMessage.trimmingCharacters(in: .whitespaces).isEmpty { 80 | messagePath = try await FileUtils().writeToTempFile( 81 | content: commitMessage, 82 | tempFileName: "squashCommitMessage" 83 | ) 84 | } 85 | 86 | let gitEditor = messagePath != nil ? "cat \"\(messagePath!)\" >" : nil 87 | 88 | result = try Rebase().rebaseInteractive( 89 | directoryURL: directoryURL, 90 | pathOfGeneratedTodo: todoPath!, 91 | lastRetainedCommitRef: lastRetainedCommitRef, 92 | action: "Squash", 93 | gitEditor: gitEditor ?? ":", 94 | progressCallback: progressCallback, 95 | commits: toSquash + [squashOnto] 96 | ) 97 | } catch { 98 | print("Error occurred: \(error)") 99 | return .error 100 | } 101 | 102 | return result 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Commands/Stage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stage.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/13. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // This source code is restricted for Aurora Editor usage only. 8 | // 9 | 10 | import Foundation 11 | 12 | public struct GitStage { 13 | 14 | public init() {} 15 | 16 | /// Stages a file with the given manual resolution method. 17 | /// Useful for resolving binary conflicts at commit-time. 18 | func stageManualConflictResolution(directoryURL: URL, 19 | file: WorkingDirectoryFileChange, 20 | manualResolution: ManualConflictResolution) throws { 21 | guard let fileStatus = file.status else { 22 | print("File status is nil") 23 | return 24 | } 25 | if !isConflictedFileStatus(fileStatus) { 26 | print("Tried to manually resolve unconflicted file (\(file.path))") 27 | return 28 | } 29 | 30 | guard let conflictedStatus = fileStatus as? ConflictsWithMarkers else { 31 | print("Failed to cast to ConflictsWithMarkers") 32 | return 33 | } 34 | 35 | if isConflictWithMarkers(conflictedStatus) && conflictedStatus.conflictMarkerCount == 0 { 36 | // If the file was manually resolved, no further action is required. 37 | return 38 | } 39 | 40 | let chosen = manualResolution == .theirs 41 | ? conflictedStatus.entry.details.them 42 | : conflictedStatus.entry.details.us 43 | 44 | let addedInBoth = conflictedStatus.entry.details.us == GitStatusEntry.added 45 | && conflictedStatus.entry.details.them == GitStatusEntry.added 46 | 47 | if chosen == .updatedButUnmerged || addedInBoth { 48 | try GitCheckout().checkoutConflictedFile(directoryURL: directoryURL, 49 | file: file, 50 | resolution: manualResolution) 51 | } 52 | 53 | switch chosen { 54 | case .deleted: 55 | try RM().removeConflictedFile(directoryURL: directoryURL, 56 | file: file) 57 | case .added, .updatedButUnmerged: 58 | try Add().addConflictedFile(directoryURL: directoryURL, 59 | file: file) 60 | default: 61 | fatalError("Unaccounted for git status entry possibility") 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Commands/Submodule.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Submodule.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/13. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // This source code is restricted for Aurora Editor usage only. 8 | // 9 | 10 | import Foundation 11 | 12 | public struct Submodule { 13 | 14 | public init() {} 15 | 16 | func listSubmodules(directoryURL: URL) throws -> [SubmoduleEntry] { 17 | let submodulesFile = FileManager.default.fileExists( 18 | atPath: directoryURL.appendingPathComponent(".gitmodules").path 19 | ) 20 | var isDirectory: ObjCBool = true 21 | let submodulesDir = FileManager.default.fileExists( 22 | atPath: directoryURL.appendingPathComponent(".git/modules").path, 23 | isDirectory: &isDirectory 24 | ) 25 | 26 | if !submodulesFile && !submodulesDir { 27 | print("No submodules found. Skipping \"git submodule status\"") 28 | return [] 29 | } 30 | 31 | let gitArgs = ["submodule", "status", "--"] 32 | let result = try GitShell().git(args: gitArgs, 33 | path: directoryURL, 34 | name: #function, 35 | options: IGitExecutionOptions(successExitCodes: Set([0, 128]))) 36 | 37 | if result.exitCode == 128 { 38 | // Unable to parse submodules in the repository, giving up 39 | return [] 40 | } 41 | 42 | var submodules = [SubmoduleEntry]() 43 | 44 | // entries are of the format: 45 | // 1eaabe34fc6f486367a176207420378f587d3b48 git (v2.16.0-rc0) 46 | // 47 | // first character: 48 | // - " " if no change 49 | // - "-" if the submodule is not initialized 50 | // - "+" if the currently checked out submodule commit does not match the SHA-1 found 51 | // in the index of the containing repository 52 | // - "U" if the submodule has merge conflicts 53 | // 54 | // then the 40-character SHA represents the current commit 55 | // 56 | // then the path to the submodule 57 | // 58 | // then the output of `git describe` for the submodule in braces 59 | // we're not leveraging this in the app, so go and read the docs 60 | // about it if you want to learn more: 61 | // 62 | // https://git-scm.com/docs/git-describe 63 | let statusRe = try NSRegularExpression(pattern: #".([^ ]+) (.+) \((.+?)\)"#, options: []) 64 | 65 | let stdout = result.stdout 66 | let range = NSRange(stdout.startIndex.. String { 60 | let components = result.stdout.components(separatedBy: "]") 61 | guard components.count >= 2, 62 | let shaPart = components.first?.components(separatedBy: " "), 63 | shaPart.count >= 2 else { 64 | return "" 65 | } 66 | return shaPart[1] 67 | } 68 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Core/GitProcess.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitProcess.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/10/31. 6 | // 7 | 8 | import Foundation 9 | 10 | struct GitProcess { 11 | 12 | // func executionOptionsWithProgress( 13 | // options: IGitExecutionOptions, 14 | // parser: GitProgressParser, 15 | // progressCallback: @escaping (GitProgressKind) -> Void 16 | // ) throws -> IGitExecutionOptions { 17 | // var lfsProgressPath: String? = nil 18 | // var env = [String: String]() 19 | // 20 | // if options.trackLFSProgress ?? false { 21 | // do { 22 | // lfsProgressPath = try createLFSProgressFile() 23 | // env["GIT_LFS_PROGRESS"] = lfsProgressPath 24 | // } catch { 25 | // print("Error writing LFS progress file", error) 26 | // env["GIT_LFS_PROGRESS"] = nil 27 | // } 28 | // } 29 | // 30 | // return merge(options, IGitExecutionOptions( 31 | // processCallback: createProgressProcessCallback( 32 | // parser: parser, 33 | // lfsProgressPath: lfsProgressPath, 34 | // progressCallback: progressCallback 35 | // ), 36 | // env: merge(options.env, env) 37 | // )) 38 | // } 39 | // 40 | // func createProgressProcessCallback(parser: GitProgressParser, 41 | // lfsProgressPath: String?, 42 | // progressCallback: @escaping (GitProgressKind) -> Void) -> (Process) -> Void { 43 | // return { process in 44 | // var lfsProgressActive = false 45 | // 46 | // if let lfsProgressPath = lfsProgressPath { 47 | // let lfsParser = GitLFSProgressParser() 48 | // let disposable = tailByLine(lfsProgressPath) { line in 49 | // let progress = lfsParser.parse(line) 50 | // 51 | // if progress.kind == "progress" { 52 | // lfsProgressActive = true 53 | // progressCallback(progress) 54 | // } 55 | // } 56 | // 57 | // process.terminationHandler = { _ in 58 | // disposable.dispose() 59 | // // the order of these callbacks is important because 60 | // // - unlink can only be done on files 61 | // // - rmdir can only be done when the directory is empty 62 | // // - we don't want to surface errors to the user if something goes 63 | // // wrong (these files can stay in TEMP and be cleaned up eventually) 64 | // do { 65 | // try FileManager.default.removeItem(atPath: lfsProgressPath) 66 | // let directory = (lfsProgressPath as NSString).deletingLastPathComponent 67 | // try? FileManager.default.removeItem(atPath: directory) 68 | // } catch { 69 | // // Handle any errors here as needed 70 | // } 71 | // } 72 | // } 73 | // 74 | // if let stderr = process.standardError { 75 | // let lineReader = LineReader(stream: stderr) 76 | // 77 | // while let line = lineReader.readLine() { 78 | // let progress = parser.parse(line) 79 | // 80 | // if lfsProgressActive { 81 | // // While we're sending LFS progress, we don't want to mix 82 | // // any non-progress events in with the output, or we'll get 83 | // // flickering between the indeterminate LFS progress and 84 | // // the regular progress. 85 | // if progress.kind == "context" { 86 | // continue 87 | // } 88 | // 89 | // let title = progress.details.title 90 | // let done = progress.details.done 91 | // 92 | // // The 'Filtering content' title happens while the LFS 93 | // // filter is running, and when it's done, we know that the 94 | // // filter is done, but until then, we don't want to display 95 | // // it for the same reason that we don't want to display 96 | // // the context above. 97 | // if title == "Filtering content" { 98 | // if done { 99 | // lfsProgressActive = false 100 | // } 101 | // continue 102 | // } 103 | // } 104 | // 105 | // progressCallback(.progress(progress)) 106 | // } 107 | // } 108 | // } 109 | // } 110 | } 111 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Core/IExecutionOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IExecutionOptions.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/10/31. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol IExecutionOptions { 11 | var env: [String: String]? { get set } 12 | var stdin: String? { get set } 13 | var stdinEncoding: String? { get set } 14 | var maxBuffer: Int? { get set } 15 | var processCallback: ((Process) -> Void)? { get set } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Core/IGitExecutionOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/10/31. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct IGitExecutionOptions: IExecutionOptions { 11 | var env: [String: String]? 12 | var stdin: String? 13 | var stdinEncoding: String? 14 | var maxBuffer: Int? 15 | var processCallback: ((Process) -> Void)? 16 | var successExitCodes: Set? 17 | var expectedErrors: Set? 18 | var trackLFSProgress: Bool? 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Core/IGitResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IGitResult.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/10/31. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct IGitResult: IResult { 11 | public var stdout: String 12 | public var stderr: String 13 | public let exitCode: Int 14 | public let gitError: GitError? 15 | public var gitErrorDescription: String? 16 | public let combinedOutput: String 17 | public let path: String 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Core/IResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IResult.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/10/31. 6 | // 7 | 8 | import Foundation 9 | 10 | /** The result of shelling out to git. */ 11 | protocol IResult { 12 | /** The standard output from git. */ 13 | var stdout: String { get } 14 | 15 | /** The standard error output from git. */ 16 | var stderr: String { get } 17 | 18 | /** The exit code of the git process. */ 19 | var exitCode: Int { get } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Core/Parsers/GitCherryPickParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitCherryPickParser.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/19. 6 | // 7 | 8 | import Foundation 9 | 10 | class GitCherryPickParser { 11 | private let commits: [Commit] 12 | private var count: Int = 0 13 | 14 | init(commits: [Commit], 15 | count: Int = 0) { 16 | self.commits = commits 17 | self.count = count 18 | } 19 | 20 | func parse(line: String) -> MultiCommitOperationProgress? { 21 | // Regular expression to match the expected cherry-pick line format 22 | let cherryPickRe = try! NSRegularExpression(pattern: "^\\[(.*\\s.*)\\]") 23 | 24 | // Range to search in the line 25 | let range = NSRange(location: 0, length: line.utf16.count) 26 | 27 | // Check if the line matches the expected format 28 | if let match = cherryPickRe.firstMatch(in: line, options: [], range: range), match.numberOfRanges > 1 { 29 | self.count += 1 // Increment the count for each matched line 30 | 31 | let summaryIndex = self.count - 1 32 | let summary = summaryIndex < commits.count ? commits[summaryIndex].summary : "" 33 | let value = Double(self.count) / Double(commits.count) 34 | 35 | return MultiCommitOperationProgress( 36 | kind: "multiCommitOperation", 37 | currentCommitSummary: summary, 38 | position: self.count, 39 | totalCommitCount: self.commits.count, 40 | value: Int(round(value * 100)) / 100 41 | ) 42 | } 43 | 44 | // Return nil if the line doesn't match 45 | return nil 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Core/Parsers/GitDelimiterParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitDelimiterParser.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/10/31. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | A utility struct for parsing Git output with custom delimiters. 12 | */ 13 | struct GitDelimiterParser { 14 | 15 | /** 16 | * Create a new parser suitable for parsing --format output from commands such 17 | * as `git log`, `git stash`, and other commands that are not derived from 18 | * `ref-filter`. 19 | * 20 | * Returns a tuple with the arguments that need to be appended to the git 21 | * call and the parse function itself 22 | * 23 | * - Parameter fields: A dictionary keyed on the friendly name of the value being 24 | * parsed with the value being the format string of said value. 25 | * 26 | * Example: 27 | * 28 | * `let (args, parse) = createLogParser(["sha": "%H"])` 29 | */ 30 | func createLogParser( 31 | _ fields: [T: String] 32 | ) -> (formatArgs: [String], parse: (String) -> [[T: String]]) { 33 | let keys = Array(fields.keys) 34 | let format = fields.values.joined(separator: "%x00") 35 | let formatArgs = ["-z", "--format=\(format)"] 36 | 37 | let parse: (String) -> [[T: String]] = { value in 38 | let records = value.components(separatedBy: "\0") 39 | var entries = [[T: String]]() 40 | 41 | for i in stride(from: 0, to: records.count - keys.count, by: keys.count) { 42 | var entry = [T: String]() 43 | for (ix, key) in keys.enumerated() { 44 | entry[key] = records[i + ix] 45 | } 46 | entries.append(entry) 47 | } 48 | 49 | return entries 50 | } 51 | 52 | return (formatArgs, parse) 53 | } 54 | 55 | /** 56 | * Create a new parser suitable for parsing --format output from commands such 57 | * as `git for-each-ref`, `git branch`, and other commands that are not derived 58 | * from `git log`. 59 | * 60 | * Returns a tuple with the arguments that need to be appended to the git 61 | * call and the parse function itself 62 | * 63 | * - Parameter fields: A dictionary keyed on the friendly name of the value being 64 | * parsed with the value being the format string of said value. 65 | * 66 | * Example: 67 | * 68 | * `let (args, parse) = createForEachRefParser(["sha": "%(objectname)"])` 69 | */ 70 | func createForEachRefParser( 71 | _ fields: [T: String] 72 | ) -> (formatArgs: [String], parse: (String) -> [[T: String]]) { 73 | let keys = Array(fields.keys) 74 | let format = fields.values.joined(separator: "%00") 75 | let formatArgs = ["--format=%00\(format)%00"] 76 | 77 | let parse: (String) -> [[T: String]] = { value in 78 | let records = value.components(separatedBy: "\0") 79 | var entries = [[T: String]]() 80 | 81 | var entry: [T: String]? 82 | var consumed = 0 83 | 84 | // start at 1 to avoid 0 modulo X problem. The first record is guaranteed 85 | // to be empty anyway (due to %00 at the start of --format) 86 | if records.count > 2 { 87 | for i in 1..<(records.count - 1) { 88 | if i % (keys.count + 1) == 0 { 89 | if records[i] != "\n" { 90 | fatalError("Expected newline") 91 | } 92 | continue 93 | } 94 | 95 | entry = entry ?? [T: String]() 96 | let key = keys[consumed % keys.count] 97 | entry![key] = records[i] 98 | consumed += 1 99 | 100 | if consumed % keys.count == 0 { 101 | entries.append(entry!) 102 | entry = nil 103 | } 104 | } 105 | } 106 | 107 | return entries 108 | } 109 | 110 | return (formatArgs, parse) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Core/Parsers/GitErrorParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitErrorParser.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/10/31. 6 | // 7 | 8 | import Foundation 9 | 10 | class GitErrorParser: Error { 11 | /// The result from the failed command. 12 | let result: IGitResult 13 | 14 | /// The args for the failed command. 15 | let args: [String] 16 | 17 | /// Whether or not the error message is just the raw output of the git command. 18 | let isRawMessage: Bool 19 | 20 | init(result: IGitResult, args: [String]) { 21 | var rawMessage = true 22 | var message = "" 23 | 24 | if let gitErrorDescription = result.gitErrorDescription { 25 | message = gitErrorDescription 26 | rawMessage = false 27 | } else if !result.combinedOutput.isEmpty { 28 | message = result.combinedOutput 29 | } else if !result.stderr.isEmpty { 30 | message = result.stderr 31 | } else if !result.stdout.isEmpty { 32 | message = result.stdout 33 | } else { 34 | message = "Unknown error" 35 | rawMessage = false 36 | } 37 | 38 | self.result = result 39 | self.args = args 40 | self.isRawMessage = rawMessage 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Core/Parsers/GitRebaseParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitRebaseParser.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/01. 6 | // 7 | 8 | import Foundation 9 | 10 | public func formatRebaseValue(value: Double) -> Double { 11 | // Clamp the value between 0 and 1 12 | let clampedValue = max(0, min(value, 1)) 13 | // Round to two decimal places 14 | let roundedValue = (clampedValue * 100).rounded() / 100 15 | return roundedValue 16 | } 17 | 18 | class GitRebaseParser { 19 | private let commits: [Commit] 20 | 21 | init(commits: [Commit]) { 22 | self.commits = commits 23 | } 24 | 25 | func parse(line: String) -> MultiCommitOperationProgress? { 26 | guard let rebasingRe = try? NSRegularExpression(pattern: "Rebasing \\((\\d+)/(\\d+)\\)") else { 27 | print("Failed to create regular expression for rebasing") 28 | return nil 29 | } 30 | let range = NSRange(location: 0, length: line.utf16.count) 31 | 32 | if let match = rebasingRe.firstMatch(in: line, options: [], range: range), 33 | match.numberOfRanges == 3, 34 | let rebasedCommitCountRange = Range(match.range(at: 1), in: line), 35 | let totalCommitCountRange = Range(match.range(at: 2), in: line), 36 | let rebasedCommitCount = Int(line[rebasedCommitCountRange]), 37 | let totalCommitCount = Int(line[totalCommitCountRange]) { 38 | 39 | let currentCommitSummary = commits.indices.contains(rebasedCommitCount - 1) 40 | ? commits[rebasedCommitCount - 1].summary 41 | : "" 42 | let progress = Double(rebasedCommitCount) / Double(totalCommitCount) 43 | let value = formatRebaseValue(value: progress) 44 | 45 | return MultiCommitOperationProgress( 46 | kind: "multiCommitOperation", 47 | currentCommitSummary: currentCommitSummary, 48 | position: rebasedCommitCount, 49 | totalCommitCount: totalCommitCount, 50 | value: Int(value) 51 | ) 52 | } 53 | return nil 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Core/ProcessError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProcessError.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2024/07/02. 6 | // 7 | 8 | // Process Specific Errors 9 | public enum ProcessError: Error { 10 | case launchFailed(String) 11 | case timeout 12 | case unexpectedExitCode(Int) 13 | case outputParsingFailed 14 | } 15 | 16 | extension ProcessError { 17 | var errorDescription: String? { 18 | switch self { 19 | case .launchFailed(let reason): 20 | return "Failed to launch process: \(reason)" 21 | case .timeout: 22 | return "Process execution timed out" 23 | case .unexpectedExitCode(let code): 24 | return "Process exited with unexpected code: \(code)" 25 | case .outputParsingFailed: 26 | return "Failed to parse process output" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Core/Progress/From-Process.swift: -------------------------------------------------------------------------------- 1 | // 2 | // From-Process.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/12. 6 | // 7 | 8 | import Foundation 9 | 10 | struct FromProcess { 11 | 12 | func executionOptionsWithProgress( 13 | options: IGitExecutionOptions, 14 | parser: GitProgressParser, 15 | progressCallback: @escaping (GitParsingResult) -> Void 16 | ) throws -> IGitExecutionOptions { 17 | var lfsProgressPath: String? 18 | var env = [String: String]() 19 | if options.trackLFSProgress! { 20 | do { 21 | lfsProgressPath = try LFSProgress().createLFSProgressFile() 22 | env["GIT_LFS_PROGRESS"] = lfsProgressPath 23 | } catch { 24 | print("Error writing LFS progress file: \(error)") 25 | env["GIT_LFS_PROGRESS"] = nil 26 | } 27 | } 28 | 29 | var mergedEnv = options.env ?? [:] 30 | mergedEnv.merge(env) { (_, new) in new } 31 | 32 | let mergedOptions = IGitExecutionOptions(env: mergedEnv, trackLFSProgress: options.trackLFSProgress) 33 | return mergedOptions 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Core/Progress/LFSProgress.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/12. 6 | // 7 | 8 | import Foundation 9 | 10 | struct IFileProgress { 11 | /// The number of bytes that have been transferred for this file. 12 | var transferred: Int 13 | 14 | /// The total size of the file in bytes. 15 | var size: Int 16 | 17 | /// Whether this file has been transferred fully. 18 | var done: Bool 19 | } 20 | 21 | struct LFSProgress { 22 | 23 | private var files = [String: IFileProgress]() 24 | 25 | let LFSProgressLineRe = try! NSRegularExpression( 26 | pattern: "^(.+?)\\s{1}(\\d+)\\/(\\d+)\\s{1}(\\d+)\\/(\\d+)\\s{1}(.+)$", 27 | options: [] 28 | ) 29 | 30 | func createLFSProgressFile() throws -> String { 31 | let tempDirectoryURL = FileManager.default.temporaryDirectory 32 | let lfsProgressURL = tempDirectoryURL.appendingPathComponent("AuroraEditor-lfs-progress-\(UUID().uuidString)") 33 | 34 | // Ensure the directory exists 35 | try FileManager.default.createDirectory(at: lfsProgressURL.deletingLastPathComponent(), 36 | withIntermediateDirectories: true) 37 | 38 | // Create the file if it does not exist 39 | if !FileManager.default.fileExists(atPath: lfsProgressURL.path) { 40 | FileManager.default.createFile(atPath: lfsProgressURL.path, 41 | contents: nil) 42 | } else { 43 | // If file exists, throw an error 44 | throw NSError(domain: "com.auroraeditor.editor", 45 | code: 0, 46 | userInfo: [NSLocalizedDescriptionKey: "File already exists"]) 47 | } 48 | 49 | return lfsProgressURL.path 50 | } 51 | 52 | mutating func parse(line: String) -> GitParsingResult { 53 | let matches = LFSProgressLineRe.matches(in: line, range: NSRange(line.startIndex..., in: line)) 54 | 55 | guard let match = matches.first, match.numberOfRanges == 7, 56 | let directionRange = Range(match.range(at: 1), in: line), 57 | let estimatedFileCountRange = Range(match.range(at: 3), in: line), 58 | let fileTransferredRange = Range(match.range(at: 4), in: line), 59 | let fileSizeRange = Range(match.range(at: 5), in: line), 60 | let fileNameRange = Range(match.range(at: 6), in: line), 61 | let estimatedFileCount = Int(line[estimatedFileCountRange]), 62 | let fileTransferred = Int(line[fileTransferredRange]), 63 | let fileSize = Int(line[fileSizeRange]) else { 64 | return IGitOutput(kind: "context", percent: 0, text: line) 65 | } 66 | 67 | let direction = String(line[directionRange]) 68 | let fileName = String(line[fileNameRange]) 69 | files[fileName] = IFileProgress(transferred: fileTransferred, size: fileSize, done: fileTransferred == fileSize) 70 | 71 | var totalTransferred = 0 72 | var totalEstimated = 0 73 | var finishedFiles = 0 74 | 75 | let fileCount = max(estimatedFileCount, files.count) 76 | 77 | for file in files.values { 78 | totalTransferred += file.transferred 79 | totalEstimated += file.size 80 | finishedFiles += file.done ? 1 : 0 81 | } 82 | 83 | let transferProgress = "\(totalTransferred) / \(totalEstimated)" 84 | 85 | let percentComplete = totalEstimated > 0 ? Int((Double(totalTransferred) / Double(totalEstimated)) * 100) : nil 86 | 87 | let verb = directionToHumanFacingVerb(direction: direction) 88 | let info = IGitProgressInfo(title: "\(verb) \"\(fileName)\"", 89 | value: totalTransferred, 90 | total: totalEstimated, 91 | percent: percentComplete, 92 | done: finishedFiles == fileCount, 93 | // swiftlint:disable:next line_length 94 | text: "\(verb) \(fileName) (\(finishedFiles) out of an estimated \(fileCount) completed, \(transferProgress))") 95 | 96 | return IGitProgress(kind: "progress", percent: info.percent ?? 0, details: info) 97 | } 98 | 99 | private func directionToHumanFacingVerb(direction: String) -> String { 100 | switch direction { 101 | case "download": 102 | return "Downloading" 103 | case "upload": 104 | return "Uploading" 105 | case "checkout": 106 | return "Checking out" 107 | default: 108 | return "Downloading" 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/CloneOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CloneOptions.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/10/31. 6 | // 7 | 8 | import Foundation 9 | 10 | struct CloneOptions { 11 | let branch: String? 12 | let defaultBranch: String? 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Diff/Diff Component Types /DiffImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiffImage.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | * A container for holding an image for display in the application 12 | */ 13 | struct DiffImage { 14 | public var contents: String 15 | public var mediaType: String 16 | public var bytes: Int 17 | 18 | /** 19 | * @param contents The base64 encoded contents of the image. 20 | * @param mediaType The data URI media type, so the browser can render the image correctly. 21 | * @param bytes Size of the file in bytes. 22 | */ 23 | public init(contents: String, 24 | mediaType: String, 25 | bytes: Int) { 26 | self.contents = contents 27 | self.mediaType = mediaType 28 | self.bytes = bytes 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Diff/Diff Types/IBinaryDiff.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IBinaryDiff.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | public class IBinaryDiff: IDiff { 11 | public var kind: DiffType = .binary 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Diff/Diff Types/IImageDiff.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IImageDiff.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct IImageDiff: IDiff { 11 | public var kind: DiffType = .image 12 | 13 | /** 14 | * The previous image, if the file was modified or deleted 15 | * 16 | * Will be undefined for an added image 17 | */ 18 | var previous: DiffImage? 19 | 20 | /** 21 | * The current image, if the file was added or modified 22 | * 23 | * Will be undefined for a deleted image 24 | */ 25 | var current: DiffImage? 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Diff/Diff Types/ILargeTextDiff.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ILargeTextDiff.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | public class ILargeTextDiff: ITextDiffData, IDiff, TextDiff { 11 | public var kind: DiffType = .largeText 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Diff/Diff Types/ISubmoduleDiff‎.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ISubmoduleDiff‎.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct ISubmoduleDiff: IDiff { 11 | public var kind: DiffType = .submodule 12 | 13 | /** Full path of the submodule */ 14 | var fullPath: String 15 | 16 | /** Path of the repository within its container repository */ 17 | var path: String 18 | 19 | /** URL of the submodule */ 20 | var url: String? 21 | 22 | /** Status of the submodule */ 23 | var status: SubmoduleStatus 24 | 25 | /** Previous SHA of the submodule, or null if it hasn't changed */ 26 | var oldSHA: String? 27 | 28 | /** New SHA of the submodule, or null if it hasn't changed */ 29 | var newSHA: String? 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Diff/Diff Types/ITextDiff.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ITextDiff.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | public class ITextDiff: ITextDiffData, TextDiff, IDiff { 11 | public var kind: DiffType = .text 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Diff/Diff Types/IUnrenderableDiff.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IUnrenderableDiff.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | public class IUnrenderableDiff: IDiff { 11 | public var kind: DiffType = .unrenderable 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Diff/DiffType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiffType.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum DiffType { 11 | /// Changes to a text file, which may be partially selected for commit 12 | case text 13 | /// Changes to a file with a known extension, which can be viewed in the editor 14 | case image 15 | /// Changes to an unknown file format, which Git is unable to present in a human-friendly format 16 | case binary 17 | /// Change to a repository which is included as a submodule of this repository 18 | case submodule 19 | /// Diff is large enough to degrade ux if rendered 20 | case largeText 21 | /// Diff that will not be rendered 22 | case unrenderable 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Diff/IDiff.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol IDiff { 11 | var kind: DiffType { get set } 12 | } 13 | 14 | public struct Diff: IDiff { 15 | public var kind: DiffType 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Diff/IDiffTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IDiffTypes.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum IDiffTypes { 11 | case text(ITextDiff) 12 | case image(IImageDiff) 13 | case binary(IBinaryDiff) 14 | case large(ILargeTextDiff) 15 | case unrenderable(IUnrenderableDiff) 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Diff/ITextDiffData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ITextDiffData.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Data returned as part of a textual diff from Aurora Editor 11 | public class ITextDiffData { 12 | /// The unified text diff - including headers and context 13 | var text: String 14 | /// The diff contents organized by hunk - how the git CLI outputs to the caller 15 | var hunks: [DiffHunk] 16 | /// A warning from Git that the line endings have changed in this file and will affect the commit 17 | var lineEndingsChange: LineEndingsChange? 18 | /// The largest line number in the diff 19 | var maxLineNumber: Int 20 | /// Whether or not the diff has invisible bidi characters 21 | var hasHiddenBidiChars: Bool 22 | 23 | init(text: String, hunks: [DiffHunk], 24 | lineEndingsChange: LineEndingsChange? = nil, 25 | maxLineNumber: Int, 26 | hasHiddenBidiChars: Bool) { 27 | self.text = text 28 | self.hunks = hunks 29 | self.lineEndingsChange = lineEndingsChange 30 | self.maxLineNumber = maxLineNumber 31 | self.hasHiddenBidiChars = hasHiddenBidiChars 32 | } 33 | } 34 | 35 | public protocol TextDiff: ITextDiffData { 36 | var kind: DiffType { get set } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Log/IChangesetData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IChangesetData.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | struct IChangesetData { 11 | /** Files changed in the changeset. */ 12 | let files: [CommittedFileChange] 13 | 14 | /** Number of lines added in the changeset. */ 15 | let linesAdded: Int 16 | 17 | /** Number of lines deleted in the changeset. */ 18 | let linesDeleted: Int 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Rebase/ComputedAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComputedAction.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ComputedAction { 11 | case clean([Commit]) 12 | case conflicts 13 | case invalid 14 | case loading 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Rebase/GitRebaseSnapshot.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitRebaseSnapshot.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | struct GitRebaseSnapshot { 11 | let commits: [Commit] 12 | let progress: MultiCommitOperationProgress 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Rebase/RebaseInternalState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RebaseInternalState.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct RebaseInternalState { 11 | /// The branch containing commits that should be rebased 12 | let targetBranch: String 13 | /// The commit ID of the base branch, to be used as a starting point for the rebase. 14 | let baseBranchTip: String 15 | /// The commit ID of the target branch at the start of the rebase, which points to the original commit history. 16 | let originalBranchTip: String 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Rebase/RebaseProgressOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RebaseProgressOptions.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | struct RebaseProgressOptions { 11 | let commits: [Commit] 12 | let progressCallback: (MultiCommitOperationProgress) -> Void 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Rebase/RebaseResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RebaseResult.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// The app-specific results from attempting to rebase a repository. 11 | enum RebaseResult { 12 | /// Git completed the rebase without reporting any errors, and the caller can signal success to the user. 13 | case completedWithoutError 14 | 15 | /// Git completed the rebase without reporting any errors, \ 16 | /// but the branch was already up to date and there was nothing to do. 17 | case alreadyUpToDate 18 | 19 | /// The rebase encountered conflicts while attempting to rebase, \ 20 | /// and these need to be resolved by the user before the rebase can continue. 21 | case conflictsEncountered 22 | 23 | /// The rebase was not able to continue as tracked files were not staged in the index. 24 | case outstandingFilesNotStaged 25 | 26 | /// The rebase was not attempted because it could not check the status of the repository. \ 27 | /// The caller needs to confirm the repository is in a usable state. 28 | case aborted 29 | 30 | /// An unexpected error as part of the rebase flow was caught and handled. 31 | /// 32 | /// Check the logs to find the relevant Git details. 33 | case error 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Status/ConflictFilesDetails.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConflictFilesDetails.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ConflictFilesDetails { 11 | let conflictCountsByPath: [String: Int] 12 | let binaryFilePaths: [String] 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Status/IStatusEntry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusEntry.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | enum StatusEntryKind { 11 | case entry 12 | } 13 | 14 | protocol IStatusEntry { 15 | var kind: StatusEntryKind { get set } 16 | var path: String { get set } 17 | var statusCode: String { get set } 18 | var submoduleStatusCode: String { get set } 19 | var oldPath: String? { get set } 20 | } 21 | 22 | struct StatusEntry: IStatusEntry { 23 | var kind: StatusEntryKind 24 | var path: String 25 | var statusCode: String 26 | var submoduleStatusCode: String 27 | var oldPath: String? 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Status/IStatusHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusHeader.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol IStatusHeader { 11 | var kind: String { get set } 12 | var value: String { get set } 13 | } 14 | 15 | struct StatusHeader: IStatusHeader { 16 | var kind: String 17 | var value: String 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Status/StatusHeadersData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusHeadersData.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | struct StatusHeadersData { 11 | let currentBranch: String? 12 | let currentUpstreamBranch: String? 13 | let currentTip: String? 14 | let branchAheadBehind: IAheadBehind? 15 | let match: [String]? 16 | 17 | public init() { 18 | self.currentBranch = nil 19 | self.currentUpstreamBranch = nil 20 | self.currentTip = nil 21 | self.branchAheadBehind = nil 22 | self.match = nil 23 | } 24 | 25 | public init(currentBranch: String?, 26 | currentUpstreamBranch: String?, 27 | currentTip: String?, 28 | branchAheadBehind: IAheadBehind?, 29 | match: [String]?) { 30 | self.currentBranch = currentBranch 31 | self.currentUpstreamBranch = currentUpstreamBranch 32 | self.currentTip = currentTip 33 | self.branchAheadBehind = branchAheadBehind 34 | self.match = match 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Status/StatusResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusResult.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct StatusResult { 11 | /// The name of the current branch. 12 | public let currentBranch: String? 13 | 14 | /// The name of the current upstream branch. 15 | public let currentUpstreamBranch: String? 16 | 17 | /// The SHA of the tip commit of the current branch. 18 | public let currentTip: String? 19 | 20 | /// Information on how many commits ahead and behind the currentBranch is compared to the currentUpstreamBranch. 21 | public let branchAheadBehind: IAheadBehind? 22 | 23 | /// True if the repository exists at the given location. 24 | public let exists: Bool 25 | 26 | /// True if the repository is in a conflicted state. 27 | public let mergeHeadFound: Bool 28 | 29 | /// True if a merge --squash operation is started. 30 | public let squashMsgFound: Bool 31 | 32 | /// Details about the rebase operation, if found. 33 | public let rebaseInternalState: RebaseInternalState? 34 | 35 | /// True if the repository is in a cherry-picking state. 36 | public let isCherryPickingHeadFound: Bool 37 | 38 | /// The absolute path to the repository's working directory. 39 | public let workingDirectory: WorkingDirectoryStatus 40 | 41 | /// Whether conflicting files are present in the repository. 42 | public let doConflictedFilesExist: Bool 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Commands/Status/WorkingDirectoryStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkingDirectoryStatus.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct WorkingDirectoryStatus { 11 | let files: [WorkingDirectoryFileChange] 12 | let includeAll: Bool? 13 | 14 | init(files: [WorkingDirectoryFileChange], 15 | includeAll: Bool? = true) { 16 | self.files = files 17 | self.includeAll = includeAll 18 | } 19 | 20 | // Computed property to create a map from file ID to index. 21 | private var fileIxById: [String: Int] { 22 | var map = [String: Int]() 23 | for (index, file) in files.enumerated() { 24 | map[file.id] = index 25 | } 26 | return map 27 | } 28 | 29 | // Static function to create a new instance with files. 30 | static func fromFiles(_ files: [WorkingDirectoryFileChange]) -> WorkingDirectoryStatus { 31 | return WorkingDirectoryStatus(files: files, includeAll: getIncludeAllState(files)) 32 | } 33 | 34 | // Function to update the include state of all files. 35 | func withIncludeAllFiles(includeAll: Bool) -> WorkingDirectoryStatus { 36 | let newFiles = files.map { $0.withIncludeAll(include: includeAll) } 37 | return WorkingDirectoryStatus(files: newFiles, includeAll: includeAll) 38 | } 39 | 40 | // Function to find a file with a given ID. 41 | func findFileWithID(_ id: String) -> WorkingDirectoryFileChange? { 42 | guard let index = fileIxById[id] else { return nil } 43 | return files.indices.contains(index) ? files[index] : nil 44 | } 45 | 46 | // Function to find the index of a file with a given ID. 47 | func findFileIndexByID(_ id: String) -> Int { 48 | return fileIxById[id] ?? -1 49 | } 50 | } 51 | 52 | func getIncludeAllState(_ files: [WorkingDirectoryFileChange]) -> Bool? { 53 | if files.isEmpty { 54 | return true 55 | } 56 | 57 | let allSelected = files.allSatisfy { 58 | $0.selection.getSelectionType() == .all 59 | } 60 | let noneSelected = files.allSatisfy { 61 | $0.selection.getSelectionType() == .none 62 | } 63 | 64 | if allSelected { 65 | return true 66 | } else if noneSelected { 67 | return false 68 | } else { 69 | return nil 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/CommitHistory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommitHistory.swift 3 | // AuroraEditorModules/Git 4 | // 5 | // Created by Marco Carnevali on 27/03/22. 6 | // 7 | 8 | import Foundation.NSDate 9 | 10 | /// Model class to help map commit history log data 11 | public struct CommitHistory: Equatable, Hashable, Identifiable { 12 | public var id = UUID() 13 | public let hash: String 14 | public let commitHash: String 15 | public let message: String 16 | public let author: String 17 | public let authorEmail: String 18 | public let commiter: String 19 | public let commiterEmail: String 20 | public let remoteURL: URL? 21 | public let date: Date 22 | public let isMerge: Bool? 23 | 24 | public init(hash: String, 25 | commitHash: String, 26 | message: String, 27 | author: String, 28 | authorEmail: String, 29 | commiter: String, 30 | commiterEmail: String, 31 | remoteURL: URL?, 32 | date: Date, 33 | isMerge: Bool?) { 34 | self.hash = hash 35 | self.commitHash = commitHash 36 | self.message = message 37 | self.author = author 38 | self.authorEmail = authorEmail 39 | self.commiter = commiter 40 | self.commiterEmail = commiterEmail 41 | self.remoteURL = remoteURL 42 | self.date = date 43 | self.isMerge = isMerge 44 | } 45 | 46 | public var commitBaseURL: URL? { 47 | if let remoteURL = remoteURL { 48 | if remoteURL.absoluteString.contains("github") { 49 | return parsedRemoteUrl(domain: "https://github.com", remote: remoteURL) 50 | } 51 | if remoteURL.absoluteString.contains("bitbucket") { 52 | return parsedRemoteUrl(domain: "https://bitbucket.org", remote: remoteURL) 53 | } 54 | if remoteURL.absoluteString.contains("gitlab") { 55 | return parsedRemoteUrl(domain: "https://gitlab.com", remote: remoteURL) 56 | } 57 | // TODO: Implement other git clients other than github, bitbucket here 58 | } 59 | return nil 60 | } 61 | 62 | private func parsedRemoteUrl(domain: String, remote: URL) -> URL { 63 | // There are 2 types of remotes - https and ssh. While https has URL in its name, ssh doesnt. 64 | // Following code takes remote name in format profileName/repoName and prepends according domain 65 | var formattedRemote = remote 66 | if formattedRemote.absoluteString.starts(with: "git@") { 67 | let parts = formattedRemote.absoluteString.components(separatedBy: ":") 68 | formattedRemote = URL.init(fileURLWithPath: "\(domain)/\(parts[parts.count - 1])") 69 | } 70 | 71 | return formattedRemote.deletingPathExtension().appendingPathComponent("commit") 72 | } 73 | 74 | public var remoteString: String { 75 | if let remoteURL = remoteURL { 76 | if remoteURL.absoluteString.contains("github") { 77 | return "GitHub" 78 | } 79 | if remoteURL.absoluteString.contains("bitbucket") { 80 | return "BitBucket" 81 | } 82 | if remoteURL.absoluteString.contains("gitlab") { 83 | return "GitLab" 84 | } 85 | } 86 | return "Remote" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/CommitIdentity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommitIdentity.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | * A tuple of name, email, and date for the author or commit 12 | * info in a commit. 13 | */ 14 | public struct CommitIdentity: Codable, Hashable { 15 | public let name: String 16 | public let email: String 17 | public let date: Date 18 | public let tzOffset: Int 19 | 20 | // Initialize the struct 21 | init(name: String, email: String, date: Date, tzOffset: Int = TimeZone.current.secondsFromGMT()) { 22 | self.name = name 23 | self.email = email 24 | self.date = date 25 | self.tzOffset = tzOffset 26 | } 27 | 28 | /** 29 | * Parses a Git ident string (GIT_AUTHOR_IDENT or GIT_COMMITTER_IDENT) 30 | * into a commit identity. Throws an error if identify string is invalid. 31 | */ 32 | static func parseIdentity(identity: String) throws -> CommitIdentity { 33 | // See fmt_ident in ident.c: 34 | // https://github.com/git/git/blob/3ef7618e6/ident.c#L346 35 | // 36 | // Format is "NAME DATE" 37 | // Jane Doe 1475670580 +0200 38 | // 39 | // Note that `git var` will strip any < and > from the name and email, see: 40 | // https://github.com/git/git/blob/3ef7618e6/ident.c#L396 41 | // 42 | // Note also that this expects a date formatted with the RAW option in git see: 43 | // https://github.com/git/git/blob/35f6318d4/date.c#L191 44 | let pattern = #"^(.*?) <(.*?)> (\d+) (\+|-)?(\d{2})(\d{2})"# 45 | 46 | if let regex = try? NSRegularExpression(pattern: pattern, options: []) { 47 | if let match = regex.firstMatch( 48 | in: identity, 49 | options: [], 50 | range: NSRange(location: 0, length: identity.utf16.count) 51 | ) { 52 | let name = (identity as NSString).substring(with: match.range(at: 1)) 53 | let email = (identity as NSString).substring(with: match.range(at: 2)) 54 | let timestamp = TimeInterval((identity as NSString).substring(with: match.range(at: 3))) ?? 0 55 | 56 | // Convert seconds since epoch to milliseconds 57 | let date = Date(timeIntervalSince1970: timestamp) 58 | 59 | // Extract the timezone offset 60 | let tzSign = (identity as NSString).substring(with: match.range(at: 4)) == "-" ? -1 : 1 61 | let tzHH = (identity as NSString).substring(with: match.range(at: 5)) 62 | let tzmm = (identity as NSString).substring(with: match.range(at: 6)) 63 | 64 | if let tzHours = Int(tzHH), let tzMinutes = Int(tzmm) { 65 | let tzOffset = tzSign * (tzHours * 60 + tzMinutes) 66 | 67 | return CommitIdentity(name: name, email: email, date: date, tzOffset: tzOffset) 68 | } 69 | } 70 | } 71 | 72 | throw NSError(domain: "", code: 0, userInfo: ["errorDescription": "Couldn't parse identity \(identity)"]) 73 | } 74 | 75 | // Convert the struct to a string 76 | func toString() -> String { 77 | return "\(name) <\(email)>" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Diff/Diff-Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Diff-Data.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/29. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | private let maximumDiffStringSize = 268435441 12 | 13 | public enum LineEndingType: String { 14 | // swiftlint:disable:next identifier_name 15 | case cr = "CR" 16 | // swiftlint:disable:next identifier_name 17 | case lf = "LF" 18 | case crlf = "CRLF" 19 | } 20 | 21 | public class LineEndingsChange { 22 | var from: LineEndingType 23 | var to: LineEndingType 24 | 25 | init(from: LineEndingType, 26 | to: LineEndingType) { 27 | self.from = from 28 | self.to = to 29 | } 30 | } 31 | 32 | /// Parse the line ending string into an enum value (or `null` if unknown) 33 | public func parseLineEndingText(text: String) -> LineEndingType? { 34 | let input = text.trimmingCharacters(in: .whitespacesAndNewlines) 35 | switch input { 36 | case "CR": 37 | return .cr 38 | case "LF": 39 | return .lf 40 | case "CRLF": 41 | return .crlf 42 | default: 43 | return nil 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Diff/Diff-Line.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Diff-Line.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/29. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Indicate what a line in the diff represents 12 | enum DiffLineType { 13 | case context 14 | case add 15 | case delete 16 | case hunk 17 | } 18 | 19 | /// Track details related to each line in the diff 20 | class DiffLine { 21 | var text: String 22 | var type: DiffLineType 23 | // Line number in the original diff patch (before expanding it), or nil if 24 | // it was added as part of a diff expansion action./ 25 | var originalLineNumber: Int? 26 | var oldLineNumber: Int? 27 | var newLineNumber: Int? 28 | var noTrailingNewLine: Bool = false 29 | 30 | init(text: String, type: DiffLineType, originalLineNumber: Int? = nil, 31 | oldLineNumber: Int? = nil, newLineNumber: Int? = nil, 32 | noTrailingNewLine: Bool) { 33 | self.text = text 34 | self.type = type 35 | self.originalLineNumber = originalLineNumber 36 | self.oldLineNumber = oldLineNumber 37 | self.newLineNumber = newLineNumber 38 | self.noTrailingNewLine = noTrailingNewLine 39 | } 40 | 41 | public func withNoTrailingNewLine(noTrailingNewLine: Bool) -> DiffLine { 42 | return DiffLine(text: self.text, 43 | type: self.type, 44 | originalLineNumber: self.originalLineNumber, 45 | oldLineNumber: self.oldLineNumber, 46 | newLineNumber: self.newLineNumber, 47 | noTrailingNewLine: noTrailingNewLine) 48 | } 49 | 50 | public func isIncludeableLine() -> Bool { 51 | return self.type == DiffLineType.add || self.type == DiffLineType.delete 52 | } 53 | 54 | /// The content of the line, i.e., without the line type marker. 55 | public func content() -> String { 56 | return self.text.substring(1) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Diff/Helper/Diff-Helper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Diff-Helper.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/29. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Utility function for getting the digit count of the largest line number in an array of diff hunks 12 | public func getLargestLineNumber(hunks: [DiffHunk]) -> Int { 13 | if hunks.isEmpty { 14 | return 0 15 | } 16 | 17 | // swiftlint:disable:next identifier_name 18 | for i in stride(from: hunks.count - 1, to: 0, by: -2) { 19 | let hunk = hunks[i] 20 | 21 | // swiftlint:disable:next identifier_name 22 | for j in stride(from: hunk.lines.count - 1, to: 0, by: -1) { 23 | let line = hunk.lines[j] 24 | 25 | let newLineNumber = line.newLineNumber ?? 0 26 | let oldLineNumber = line.oldLineNumber ?? 0 27 | return newLineNumber > oldLineNumber ? newLineNumber : oldLineNumber 28 | } 29 | } 30 | 31 | return 0 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Diff/Raw-Diff.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Raw-Diff.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/29. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum DiffHunkExpansionType: String { 12 | /// The hunk header cannot be expanded at all. 13 | case none = "None" 14 | 15 | /// The hunk header can be expanded up exclusively. Only the first hunk can be 16 | /// expanded up exclusively. 17 | case up = "Up" // swiftlint:disable:this identifier_name 18 | 19 | /// The hunk header can be expanded down exclusively. Only the last hunk (if 20 | /// it's the dummy hunk with only one line) can be expanded down exclusively. 21 | case down = "Down" 22 | 23 | /// The hunk header can be expanded both up and down. 24 | case both = "Both" 25 | 26 | /// The hunk header represents a short gap that, when expanded, will 27 | /// result in merging this hunk and the hunk above. 28 | case short = "Short" 29 | } 30 | 31 | /// Each diff is made up of a number of hunks 32 | public class DiffHunk { 33 | var header: DiffHunkHeader 34 | var lines: [DiffLine] 35 | var unifiedDiffStart: Int 36 | var unifiedDiffEnd: Int 37 | var expansionType: DiffHunkExpansionType 38 | 39 | init(header: DiffHunkHeader, 40 | lines: [DiffLine], 41 | unifiedDiffStart: Int, 42 | unifiedDiffEnd: Int, 43 | expansionType: DiffHunkExpansionType) { 44 | self.header = header 45 | self.lines = lines 46 | self.unifiedDiffStart = unifiedDiffStart 47 | self.unifiedDiffEnd = unifiedDiffEnd 48 | self.expansionType = expansionType 49 | } 50 | } 51 | 52 | class DiffHunkHeader { 53 | var oldStartLine: Int 54 | var oldLineCount: Int 55 | var newStartLine: Int 56 | var newLineCount: Int 57 | 58 | init(oldStartLine: Int, oldLineCount: Int, newStartLine: Int, newLineCount: Int) { 59 | self.oldStartLine = oldStartLine 60 | self.oldLineCount = oldLineCount 61 | self.newStartLine = newStartLine 62 | self.newLineCount = newLineCount 63 | } 64 | 65 | public func toDiffLineRepresentation() -> String { 66 | return "@@ -\(self.oldStartLine),\(self.oldLineCount) +\(self.newStartLine),\(self.newLineCount) @@" 67 | } 68 | } 69 | 70 | public class IRawDiff { 71 | /// The plain text contents of the diff header. This contains 72 | /// everything from the start of the diff up until the first 73 | /// hunk header starts. Note that this does not include a trailing 74 | /// newline. 75 | var header: String 76 | 77 | /// The plain text contents of the diff. This contains everything 78 | /// after the diff header until the last character in the diff. 79 | /// 80 | /// Note that this does not include a trailing newline nor does 81 | /// it include diff 'no newline at end of file' comments. For 82 | /// no-newline information, consult the DiffLine noTrailingNewLine 83 | /// property. 84 | var contents: String 85 | 86 | /// Each hunk in the diff with information about start, and end 87 | /// positions, lines and line statuses. 88 | var hunks: [DiffHunk] 89 | 90 | /// Whether or not the unified diff indicates that the contents 91 | /// could not be diffed due to one of the versions being binary. 92 | var isBinary: Bool 93 | 94 | /// The largest line number in the diff 95 | var maxLineNumber: Int 96 | 97 | /// Whether or not the diff has invisible bidi characters 98 | var hasHiddenBidiChars: Bool 99 | 100 | init(header: String, 101 | contents: String, 102 | hunks: [DiffHunk], 103 | isBinary: Bool, 104 | maxLineNumber: Int, 105 | hasHiddenBidiChars: Bool) { 106 | self.header = header 107 | self.contents = contents 108 | self.hunks = hunks 109 | self.isBinary = isBinary 110 | self.maxLineNumber = maxLineNumber 111 | self.hasHiddenBidiChars = hasHiddenBidiChars 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Files/AppFileStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppFileStatus.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/10/31. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum GitStatusEntry: String, Codable { 11 | case modified = "M" 12 | case added = "A" 13 | case deleted = "D" 14 | case renamed = "R" 15 | case copied = "C" 16 | case unchanged = "." 17 | case untracked = "?" 18 | case ignored = "!" 19 | case updatedButUnmerged = "U" 20 | } 21 | 22 | public enum AppFileStatusKind: String, Codable { 23 | case new = "New" 24 | case modified = "Modified" 25 | case deleted = "Deleted" 26 | case copied = "Copied" 27 | case renamed = "Renamed" 28 | case conflicted = "Conflicted" 29 | case untracked = "Untracked" 30 | } 31 | 32 | public struct SubmoduleStatus: Codable { 33 | let commitChanged: Bool 34 | let modifiedChanges: Bool 35 | let untrackedChanges: Bool 36 | } 37 | 38 | public struct PlainFileStatus: AppFileStatus, Codable { 39 | public var kind: AppFileStatusKind 40 | public var submoduleStatus: SubmoduleStatus? 41 | } 42 | 43 | public struct CopiedOrRenamedFileStatus: AppFileStatus, Codable { 44 | public var kind: AppFileStatusKind 45 | let oldPath: String 46 | public var submoduleStatus: SubmoduleStatus? 47 | } 48 | 49 | // MARK: - Conflicted 50 | public protocol ConflictedFileStatus: AppFileStatus {} 51 | 52 | public struct ConflictsWithMarkers: ConflictedFileStatus, Codable { 53 | public var kind: AppFileStatusKind 54 | let entry: TextConflictEntry 55 | let conflictMarkerCount: Int 56 | public var submoduleStatus: SubmoduleStatus? 57 | } 58 | 59 | public struct ManualConflict: ConflictedFileStatus, Codable { 60 | public var kind: AppFileStatusKind 61 | let entry: ManualConflictEntry 62 | public var submoduleStatus: SubmoduleStatus? 63 | } 64 | 65 | public func isConflictedFileStatus(_ appFileStatus: AppFileStatus) -> Bool { 66 | return appFileStatus.kind == .conflicted 67 | } 68 | 69 | public func isConflictWithMarkers(_ conflictedFileStatus: ConflictedFileStatus) -> Bool { 70 | return conflictedFileStatus is ConflictsWithMarkers 71 | } 72 | 73 | public func isManualConflict(_ conflictedFileStatus: ConflictedFileStatus) -> Bool { 74 | return conflictedFileStatus is ManualConflict 75 | } 76 | 77 | public struct UntrackedFileStatus: AppFileStatus, Codable { 78 | public var kind: AppFileStatusKind 79 | public var submoduleStatus: SubmoduleStatus? 80 | } 81 | 82 | public protocol AppFileStatus: Codable { 83 | var kind: AppFileStatusKind { get set } 84 | var submoduleStatus: SubmoduleStatus? { get set } 85 | } 86 | 87 | public enum UnmergedEntrySummary: String, Codable { 88 | case AddedByUs = "added-by-us" 89 | case DeletedByUs = "deleted-by-us" 90 | case AddedByThem = "added-by-them" 91 | case DeletedByThem = "deleted-by-them" 92 | case BothDeleted = "both-deleted" 93 | case BothAdded = "both-added" 94 | case BothModified = "both-modified" 95 | } 96 | 97 | public struct ManualConflictDetails: Codable { 98 | let submoduleStatus: SubmoduleStatus? 99 | let action: UnmergedEntrySummary 100 | let us: GitStatusEntry 101 | let them: GitStatusEntry 102 | } 103 | 104 | public struct TextConflictDetails: Codable { 105 | let action: UnmergedEntrySummary 106 | let us: GitStatusEntry 107 | let them: GitStatusEntry 108 | } 109 | 110 | // MARK: - Entry Conformities 111 | 112 | protocol FileEntry { 113 | var kind: String { get } 114 | var submoduleStatus: SubmoduleStatus? { get } 115 | } 116 | 117 | protocol UnmergedEntry {} 118 | 119 | public struct TextConflictEntry: Codable, FileEntry, UnmergedEntry { 120 | let kind: String = "conflicted" 121 | let submoduleStatus: SubmoduleStatus? 122 | let details: TextConflictDetails 123 | } 124 | 125 | public struct ManualConflictEntry: Codable, FileEntry, UnmergedEntry { 126 | let kind: String = "conflicted" 127 | let submoduleStatus: SubmoduleStatus? 128 | let details: ManualConflictDetails 129 | } 130 | 131 | struct UntrackedEntry: FileEntry { 132 | let kind: String = "untracked" 133 | let submoduleStatus: SubmoduleStatus? 134 | } 135 | 136 | struct RenamedOrCopiedEntry: FileEntry { 137 | enum RenamedOrCopiedEntryType: String { 138 | case renamed 139 | case copied 140 | } 141 | 142 | let kind: String 143 | let index: GitStatusEntry? 144 | let workingTree: GitStatusEntry? 145 | let submoduleStatus: SubmoduleStatus? 146 | 147 | init(kind: RenamedOrCopiedEntryType, 148 | index: GitStatusEntry?, 149 | workingTree: GitStatusEntry?, 150 | submoduleStatus: SubmoduleStatus?) { 151 | self.kind = kind.rawValue 152 | self.index = index 153 | self.workingTree = workingTree 154 | self.submoduleStatus = submoduleStatus 155 | } 156 | 157 | } 158 | 159 | struct OrdinaryEntry: FileEntry { 160 | 161 | enum OrdinaryEntryType { 162 | case added 163 | case modified 164 | case deleted 165 | } 166 | 167 | let kind: String = "ordinary" 168 | let type: OrdinaryEntryType 169 | let index: GitStatusEntry? 170 | let workingTree: GitStatusEntry? 171 | let submoduleStatus: SubmoduleStatus? 172 | } 173 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Files/CommittedFileChange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommittedFileChange.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/10/31. 6 | // 7 | 8 | import Foundation 9 | 10 | class CommittedFileChange: FileChange { 11 | let commitish: String 12 | let parentCommitish: String 13 | 14 | init(path: String, 15 | status: AppFileStatus, 16 | commitish: String, 17 | parentCommitish: String) { 18 | self.commitish = commitish 19 | self.parentCommitish = parentCommitish 20 | super.init(path: path, status: status) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Files/FileChange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileChange.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/10/31. 6 | // 7 | 8 | import Foundation 9 | 10 | open class FileChange { 11 | let id: String 12 | let path: String 13 | let status: AppFileStatus? 14 | 15 | public init(path: String, 16 | status: AppFileStatus?) { 17 | self.path = path 18 | self.status = status 19 | 20 | var fileId: String = "" 21 | 22 | // Generate a unique identifier based on the status and path. 23 | if let plainStatus = status as? PlainFileStatus { 24 | fileId = "plain+\(plainStatus.kind)+\(path)" 25 | } else if let copiedOrRenamedStatus = status as? CopiedOrRenamedFileStatus { 26 | fileId = "copiedOrRenamed+\(copiedOrRenamedStatus.oldPath)->\(path)" 27 | } else if status is ConflictsWithMarkers { 28 | fileId = "conflictsWithMarkers+\(path)" 29 | } else if status is ManualConflict { 30 | fileId = "manualConflict+\(path)" 31 | } else if status is UntrackedFileStatus { 32 | fileId = "untracked+\(path)" 33 | } else { 34 | print("Unknown AppFileStatus type") 35 | } 36 | 37 | self.id = fileId 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Files/WorkingDirectoryFileChange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkingDirectoryFileChange.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/13. 6 | // 7 | 8 | import Foundation 9 | 10 | open class WorkingDirectoryFileChange: FileChange { 11 | let selection: DiffSelection 12 | 13 | public init(path: String, 14 | status: AppFileStatus?, 15 | selection: DiffSelection) { 16 | self.selection = selection 17 | super.init(path: path, status: status) 18 | } 19 | 20 | func withIncludeAll(include: Bool) -> WorkingDirectoryFileChange { 21 | let newSelection = include ? selection.withSelectAll() : selection.withSelectNone() 22 | return withSelection(newSelection) 23 | } 24 | 25 | func withSelection(_ selection: DiffSelection) -> WorkingDirectoryFileChange { 26 | return WorkingDirectoryFileChange(path: self.path, status: self.status, selection: selection) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/GitCommit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Commit.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/15. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // This source code is restricted for Aurora Editor usage only. 8 | // 9 | 10 | import Foundation 11 | 12 | func shortenSHA(_ sha: String) -> String { 13 | return String(sha.prefix(9)) 14 | } 15 | 16 | /// Grouping of information required to create a commit 17 | public protocol ICommitContext { 18 | /// The summary of the commit message (required) 19 | var summary: String? { get } 20 | /// Additional details for the commit message (optional) 21 | var description: String? { get } 22 | /// Whether or not it should amend the last commit (optional, default: false) 23 | var amend: Bool? { get } 24 | /// An optional array of commit trailers (for example Co-Authored-By trailers) 25 | /// which will be appended to the commit message in accordance with the Git trailer configuration. 26 | var trailers: [Trailer]? { get } 27 | } 28 | 29 | public struct CommitContext: ICommitContext { 30 | public var summary: String? 31 | public var description: String? 32 | public var amend: Bool? 33 | public var trailers: [Trailer]? 34 | 35 | public init(summary: String?, 36 | description: String?, 37 | amend: Bool?, 38 | trailers: [Trailer]?) { 39 | self.summary = summary 40 | self.description = description 41 | self.amend = amend 42 | self.trailers = trailers 43 | } 44 | } 45 | 46 | /// Extract any Co-Authored-By trailers from an array of arbitrary 47 | /// trailers. 48 | public func extractCoAuthors(trailers: [Trailer]) -> [GitAuthor] { 49 | var coAuthors: [GitAuthor] = [] 50 | 51 | for trailer in trailers where InterpretTrailers().isCoAuthoredByTrailer(trailer: trailer) { 52 | let author = GitAuthor(name: nil, email: nil).parse(nameAddr: trailer.value) 53 | if author != nil { 54 | coAuthors.append(author!) 55 | } 56 | } 57 | 58 | return coAuthors 59 | } 60 | 61 | /// A git commit. 62 | public struct Commit: Codable, Equatable, Identifiable, Hashable { 63 | public var id = UUID() 64 | 65 | /// A list of co-authors parsed from the commit message 66 | /// trailers. 67 | public var coAuthors: [GitAuthor]? 68 | /// The commit body after removing coauthors 69 | public var bodyNoCoAuthors: String? 70 | /// A value indicating whether the author and the committer 71 | /// are the same person. 72 | public var authoredByCommitter: Bool 73 | /// Whether or not the commit is a merge commit (i.e. has at least 2 parents) 74 | public var isMergeCommit: Bool 75 | 76 | public var sha: String 77 | public var shortSha: String 78 | public var summary: String 79 | public var body: String 80 | public var author: CommitIdentity 81 | public var committer: CommitIdentity 82 | public var parentSHAs: [String] 83 | public var trailers: [Trailer] 84 | public var tags: [String] 85 | 86 | public init(sha: String, 87 | shortSha: String, 88 | summary: String, 89 | body: String, 90 | author: CommitIdentity, 91 | commiter: CommitIdentity, 92 | parentShas: [String], 93 | trailers: [Trailer], 94 | tags: [String]) { 95 | self.sha = sha 96 | self.shortSha = shortSha 97 | self.summary = summary 98 | self.body = body 99 | self.author = author 100 | self.committer = commiter 101 | self.parentSHAs = parentShas 102 | self.trailers = trailers 103 | self.tags = tags 104 | 105 | self.coAuthors = extractCoAuthors(trailers: trailers) 106 | self.authoredByCommitter = (author.name == committer.name && author.email == committer.email) 107 | self.bodyNoCoAuthors = InterpretTrailers().trimCoAuthorsTrailers(trailers: trailers, body: body) 108 | self.isMergeCommit = parentShas.count > 1 109 | } 110 | 111 | public init(sha: String, 112 | summary: String) { 113 | self.sha = sha 114 | self.summary = summary 115 | 116 | self.shortSha = "" 117 | self.body = "" 118 | self.author = CommitIdentity(name: "", 119 | email: "", 120 | date: Date()) 121 | self.committer = CommitIdentity(name: "", 122 | email: "", 123 | date: Date()) 124 | self.parentSHAs = [] 125 | self.trailers = [] 126 | self.tags = [] 127 | 128 | self.coAuthors = nil 129 | self.authoredByCommitter = false 130 | self.bodyNoCoAuthors = nil 131 | self.isMergeCommit = false 132 | } 133 | 134 | public static func == (lhs: Commit, rhs: Commit) -> Bool { 135 | return lhs.sha == rhs.sha 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/GitFileItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitFileItem.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2022/10/05. 6 | // 7 | 8 | import Foundation 9 | 10 | @available(macOS, deprecated, message: "Use `FileChange` instead") 11 | public protocol GitFileItem: Codable { 12 | 13 | var gitStatus: GitType? { get set } 14 | 15 | /// Returns the URL of the ``FileSystemClient/FileSystemClient/FileItem`` 16 | var url: URL { get set } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/GitType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitType.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2022/05/20. 6 | // 7 | 8 | import Foundation 9 | 10 | // Used to determine the git type 11 | @available(macOS, deprecated, message: "We use the new AppFileStatus protocol") 12 | public enum GitType: String, Codable { 13 | case modified = "M" 14 | case unknown = "??" 15 | case fileTypeChange = "T" 16 | case added = "A" 17 | case deleted = "D" 18 | case renamed = "R" 19 | case copied = "C" 20 | case updatedUnmerged = "U" 21 | case ignored = "!" 22 | case unchanged = "." 23 | 24 | public var description: String { 25 | switch self { 26 | case .modified: return "M" 27 | case .unknown: return "?" 28 | case .fileTypeChange: return "T" 29 | case .added: return "A" 30 | case .deleted: return "D" 31 | case .renamed: return "R" 32 | case .copied: return "C" 33 | case .updatedUnmerged: return "U" 34 | case .ignored: return "!" 35 | case .unchanged: return "." 36 | } 37 | } 38 | } 39 | 40 | /// The enum representation of a Git file change in Aurora Editor. 41 | @available(macOS, deprecated, message: "We use the new AppFileStatus protocol") 42 | enum FileStatusKind: String { 43 | case new = "New" 44 | case modified = "Modified" 45 | case deleted = "Deleted" 46 | case copied = "Copied" 47 | case renamed = "Renamed" 48 | case conflicted = "Conflicted" 49 | case untracked = "Untracked" 50 | } 51 | 52 | /// The porcelain status for an unmerged entry 53 | @available(macOS, deprecated, message: "We use the new AppFileStatus protocol") 54 | func untrackedEntry() -> String { 55 | return "untracked" 56 | } 57 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/IGitAccount.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IGitAccount.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/10/31. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | * An account which can be used to potentially authenticate with a git server. 12 | */ 13 | public struct IGitAccount { 14 | 15 | /** The login/username to authenticate with. */ 16 | let login: String 17 | 18 | /** The endpoint with which the user is authenticating. */ 19 | let endpoint: String 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/IRemote.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IRemote.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/12. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // This source code is restricted for Aurora Editor usage only. 8 | // 9 | 10 | import Foundation 11 | 12 | private var forkedRemotePrefix = "aurora-editor-" 13 | 14 | public func forkPullRequestRemoteName(remoteName: String) -> String { 15 | return "\(forkedRemotePrefix)\(remoteName)" 16 | } 17 | 18 | public protocol IRemote { 19 | var name: String { get } 20 | var url: String { get } 21 | } 22 | 23 | public struct GitRemote: IRemote, Hashable { 24 | public var id: String { self.name } 25 | public var name: String 26 | public var url: String 27 | 28 | init(name: String, url: String) { 29 | self.name = name 30 | self.url = url 31 | } 32 | 33 | public static func == (lhs: GitRemote, rhs: GitRemote) -> Bool { 34 | return lhs.name == rhs.name 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/ManualConflictResolution.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ManualConflictResolution.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/15. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // This source code is restricted for Aurora Editor usage only. 8 | // 9 | 10 | import Foundation 11 | 12 | // NOTE: These strings have semantic value, they're passed directly 13 | // as `--ours` and `--theirs` to git checkout. Please be careful 14 | // when modifying this type. 15 | public enum ManualConflictResolution: String { 16 | case theirs = "theirs" // swiftlint:disable:this redundant_string_enum_value 17 | case ours = "ours" // swiftlint:disable:this redundant_string_enum_value 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Version-Control/Base/Models/Stash-Entry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stash-Entry.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/15. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // This source code is restricted for Aurora Editor usage only. 8 | // 9 | 10 | import Foundation 11 | 12 | protocol IStashEntry { 13 | /// The fully qualified name of the entry i.e., `refs/stash@{0}` 14 | var name: String? { get } 15 | /// The name of the branch at the time the entry was created. 16 | var branchName: String? { get } 17 | /// The SHA of the commit object created as a result of stashing. 18 | var stashSha: String? { get } 19 | /// The list of files this stash touches 20 | var files: GitFileItem? { get } 21 | 22 | var tree: String? { get } 23 | var parents: [String]? { get } 24 | } 25 | 26 | class StashEntry: IStashEntry { 27 | var name: String? 28 | var branchName: String? 29 | var stashSha: String? 30 | var files: GitFileItem? 31 | var tree: String? 32 | var parents: [String]? 33 | 34 | init(name: String?, 35 | branchName: String?, 36 | stashSha: String?, 37 | files: GitFileItem?, 38 | tree: String?, 39 | parents: [String]?) { 40 | self.branchName = branchName 41 | self.name = name 42 | self.stashSha = stashSha 43 | self.files = files 44 | self.tree = tree 45 | self.parents = parents 46 | } 47 | } 48 | 49 | /// Whether file changes for a stash entry are loaded or not 50 | enum StashedChangesLoadStates: String { 51 | case notLoaded = "NotLoaded" 52 | case loading = "Loading" 53 | case loaded = "Loaded" 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Version-Control/Errors/IndexError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IndexError.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/16. 6 | // 7 | 8 | import Foundation 9 | 10 | enum IndexError: Error { 11 | case unknownIndex(String) 12 | case noRenameIndex(String) 13 | case invalidStatus(String) 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Version-Control/Errors/NetworkingError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkingError.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/26. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum NetworkingError: Error { 11 | case invalidURL 12 | case noData 13 | case invalidResponse 14 | case serverError(statusCode: Int, data: Data) 15 | case encodingFailed(Error) 16 | case customError(message: String) 17 | 18 | public var localizedDescription: String { 19 | switch self { 20 | case .invalidURL: 21 | return "The URL provided was invalid." 22 | case .noData: 23 | return "No data was received from the server." 24 | case .invalidResponse: 25 | return "The response received from the server was invalid." 26 | case .serverError(let statusCode, let data): 27 | let errorMessage = String(data: data, encoding: .utf8) ?? "Unknown server error" 28 | return "Server error with status code \(statusCode): \(errorMessage)" 29 | case .encodingFailed(let error): 30 | return "Failed to encode parameters: \(error.localizedDescription)" 31 | case .customError(let message): 32 | return message 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Version-Control/Errors/ShellErrors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/25. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ShellErrors: String, Error { 11 | // swiftlint:disable:next line_length 12 | case failedToInitializeRepository = "An error occurred while attempting to initialize the Git repository. Possible reasons for this failure include:\n\n1. The specified directory does not exist or is inaccessible.\n2. Git is not installed on your system, or it is not in the system's PATH.\n3. There may be a conflict with an existing Git repository in the specified directory.\n4. The Git initialization command encountered an unexpected issue." 13 | // swiftlint:disable:next line_length 14 | case failedToInstallLFS = "An error occurred while attempting to install Git Large File Storage (LFS). Possible reasons for this failure include:\n\n1. There may be a network issue preventing the installation of Git LFS.\n2. The Git LFS installation command encountered an unexpected issue." 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/BitBucket/Interfaces/Repo/IBitBucketAPIPullRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IBitBucketAPIPullRequest.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/10/30. 6 | // 7 | 8 | import Foundation 9 | 10 | struct IBitBucketAPIPullRequest: Codable { 11 | public let id: Int 12 | public let title: String 13 | public let created_on: String 14 | public let updated_on: String 15 | public let author: IAPIIdentity 16 | public let summary: IBitbucketRenderedBody 17 | public let state: String 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/BitBucket/Interfaces/Repo/IBitbucketComment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IBitbucketComment.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/10/31. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents both issue comments and PR review comments. 12 | */ 13 | public struct IBitbucketComment: Codable { 14 | public let id: Int 15 | public let body: String 16 | public let html_url: String 17 | public let user: IAPIIdentity 18 | public let created_at: String 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/BitBucket/Interfaces/Repo/IBitbucketRendered.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IBitbucketRendered.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/10/31. 6 | // 7 | 8 | import Foundation 9 | 10 | struct IBitbucketRendered { 11 | var description: IBitbucketRenderedBody 12 | } 13 | 14 | struct IBitbucketRenderedBody: Codable { 15 | var raw: String 16 | var markup: String 17 | var html: String 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/AuthorizationResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthorizationResponse.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Struct representing an authorization response. 11 | struct AuthorizationResponse { 12 | /// The kind of authorization response. 13 | let kind: AuthorizationResponseKind 14 | 15 | /// The token associated with successful authorization. 16 | let token: String? 17 | 18 | /// The HTTP response associated with failed authorization. 19 | let response: String? 20 | 21 | /// The type of authentication mode required for two-factor authentication. 22 | let type: AuthenticationMode? 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/AuthorizationResponseKind.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthorizationResponseKind.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | /// Enum representing different kinds of authorization responses. 9 | enum AuthorizationResponseKind { 10 | /// The authorization was successful. 11 | case authorized 12 | 13 | /// The authorization failed. 14 | case failed 15 | 16 | /// Two-factor authentication is required. 17 | case twoFactorAuthenticationRequired 18 | 19 | /// User verification is required. 20 | case userRequiresVerification 21 | 22 | /// Personal access token is blocked. 23 | case personalAccessTokenBlocked 24 | 25 | /// An error occurred during authorization. 26 | case error 27 | 28 | /// The enterprise is too old for the authorization. 29 | case enterpriseTooOld 30 | 31 | /// Web authentication flow is required. 32 | /// 33 | /// The API has indicated that the user is required to go through 34 | /// the web authentication flow. 35 | case webFlowRequired 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/GithubNetworkingConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkingConstant.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/09/13. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // swiftlint:disable:next convenience_type 12 | struct GithubNetworkingConstants { 13 | static var baseURL: String = "https://api.github.com/" 14 | 15 | // GitHub Actions 16 | 17 | // MARK: Workflows 18 | static func workflows(_ owner: String, _ repo: String) -> String { 19 | return "repos/\(owner)/\(repo)/actions/workflows" 20 | } 21 | 22 | static func workflow(_ owner: String, 23 | _ repo: String, 24 | workflowId: String) -> String { 25 | return "repos/\(owner)/\(repo)/actions/workflows/\(workflowId)" 26 | } 27 | 28 | static func workflowRuns(_ owner: String, 29 | _ repo: String, 30 | workflowId: String) -> String { 31 | return "repos/\(owner)/\(repo)/actions/workflows/\(workflowId)/runs" 32 | } 33 | 34 | // MARK: Workflow Runs 35 | static func reRunWorkflow(_ owner: String, 36 | _ repo: String, 37 | runId: String) -> String { 38 | return "repos/\(owner)/\(repo)/actions/runs/\(runId)/rerun" 39 | } 40 | 41 | static func cancelWorkflow(_ owner: String, 42 | _ repo: String, 43 | runId: String) -> String { 44 | return "repos/\(owner)/\(repo)/actions/runs/\(runId)/cancel" 45 | } 46 | 47 | // MARK: Workflow Jobs 48 | static func reRunJob(_ owner: String, 49 | _ repo: String, 50 | jobId: String) -> String { 51 | return "repos/\(owner)/\(repo)/actions/jobs/\(jobId)/rerun" 52 | } 53 | 54 | static func workflowJob(_ owner: String, 55 | _ repo: String, 56 | jobId: String) -> String { 57 | return "repos/\(owner)/\(repo)/actions/jobs/\(jobId)" 58 | } 59 | 60 | static func workflowJobs(_ owner: String, 61 | _ repo: String, 62 | runId: String) -> String { 63 | return "repos/\(owner)/\(repo)/actions/runs/\(runId)/jobs" 64 | } 65 | 66 | static func downloadWorkflowJobLog(_ owner: String, 67 | _ repo: String, 68 | jobId: String) -> String { 69 | return "repos/\(owner)/\(repo)/actions/jobs/\(jobId)/logs" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Account/IAPIEmail.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIEmail.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /// `null` can be returned by the API for legacy reasons. A non-null value is 11 | /// set for the primary email address currently, but in the future visibility 12 | /// may be defined for each email address. 13 | public enum EmailVisibility: String, Codable { 14 | case `public` = "public" 15 | case `private` = "private" 16 | case `null` = "" 17 | } 18 | 19 | /// Information about a user's email as returned by the GitHub API. 20 | public struct IAPIEmail: Codable { 21 | let email: String 22 | let verified: Bool 23 | let primary: Bool 24 | let visibility: EmailVisibility 25 | 26 | public init( 27 | email: String, 28 | verified: Bool, 29 | primary: Bool, 30 | visibility: EmailVisibility 31 | ) { 32 | self.email = email 33 | self.verified = verified 34 | self.primary = primary 35 | self.visibility = visibility 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Account/IAPIFullIdentity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIFullIdentity.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | * Complete identity details returned in some situations by the GitHub API. 12 | * 13 | * If you are not sure what is returned as part of an API response, you should 14 | * use `IAPIIdentity` as that contains the known subset of an identity and does 15 | * not cover scenarios where privacy settings of a user control what information 16 | * is returned. 17 | */ 18 | struct IAPIFullIdentity: Codable { 19 | let id: Int 20 | let htmlUrl: String 21 | let login: String 22 | let avatarUrl: String 23 | 24 | /** 25 | * The user's real name or null if the user hasn't provided 26 | * a real name for their public profile. 27 | */ 28 | let name: String? 29 | 30 | /** 31 | * The email address for this user or null if the user has not 32 | * specified a public email address in their profile. 33 | */ 34 | let email: String? 35 | let type: GitHubAccountType 36 | let plan: Plan? 37 | 38 | struct Plan: Codable { 39 | let name: String 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Account/IAPIIdentity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIIdentity.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Minimum subset of an identity returned by the GitHub API. 12 | */ 13 | public struct IAPIIdentity: Codable { 14 | public let id: Int 15 | public let login: String 16 | public let avatar_url: String 17 | public let html_url: String 18 | public let type: GitHubAccountType 19 | } 20 | 21 | /** 22 | Enumeration to represent the type of GitHub account. 23 | */ 24 | public enum GitHubAccountType: String, Codable { 25 | case user = "User" 26 | case organization = "Organization" 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Account/IAPIOrganization.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIOrganization.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | * Entity returned by the `/user/orgs` endpoint. 12 | * 13 | * Because this is specific to one endpoint it omits the `type` member from 14 | * `IAPIIdentity` that callers might expect. 15 | */ 16 | struct IAPIOrganization: Codable { 17 | let id: Int 18 | let url: String 19 | let login: String 20 | let avatarUrl: String 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Branch/IAPIBranch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIBranch.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | /** 9 | Branch information returned by the GitHub API. 10 | */ 11 | public struct IAPIBranch: Codable { 12 | /** 13 | The name of the branch stored on the remote. 14 | 15 | NOTE: This is NOT a fully-qualified ref (i.e., `refs/heads/main`). 16 | */ 17 | public let name: String 18 | 19 | /** 20 | Branch protection settings: 21 | 22 | - `true` indicates that the branch is protected in some way. 23 | - `false` indicates no branch protection set. 24 | */ 25 | public let protected: Bool 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/IAPIFullRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIFullRepository.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct IAPIFullRepository: Codable { 11 | 12 | /** 13 | * The parent repository of a fork. 14 | * 15 | * HACK: BEWARE: This is defined as `parent: IAPIRepository | undefined` 16 | * rather than `parent?: ...` even though the parent property is actually 17 | * optional in the API response. So we're lying a bit to the type system 18 | * here saying that this will be present but the only time the difference 19 | * between omission and explicit undefined matters is when using constructs 20 | * like `x in y` or `y.hasOwnProperty('x')` which we do very rarely. 21 | * 22 | * Without at least one non-optional type in this interface TypeScript will 23 | * happily let us pass an IAPIRepository in place of an IAPIFullRepository. 24 | */ 25 | let parent: IAPIRepository? 26 | let cloneUrl: String 27 | let sshUrl: String 28 | let htmlUrl: String 29 | let name: String 30 | let owner: IAPIIdentity 31 | let isPrivate: Bool 32 | let isFork: Bool 33 | let defaultBranch: String 34 | let pushedAt: String 35 | let hasIssues: Bool 36 | let isArchived: Bool 37 | 38 | /** 39 | * The high-level permissions that the currently authenticated 40 | * user enjoys for the repository. Undefined if the API call 41 | * was made without an authenticated user or if the repository 42 | * isn't the primarily requested one (i.e. if this is the parent 43 | * repository of the requested repository) 44 | * 45 | * The permissions hash will also be omitted when the repository 46 | * information is embedded within another object such as a pull 47 | * request (base.repo or head.repo). 48 | * 49 | * In other words, the only time when the permissions property 50 | * will be present is when explicitly fetching the repository 51 | * through the `/repos/user/name` endpoint or similar. 52 | */ 53 | let permissions: IAPIRepositoryPermissions? 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/IAPIMentionableResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIMentionableResponse.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2024/07/15. 6 | // 7 | 8 | public struct IAPIMentionableResponse: Codable { 9 | public let etag: String? 10 | public let users: [IAPIMentionableUser] 11 | } 12 | 13 | public struct IAPIMentionableUser: Codable { 14 | /// The username or "handle" of the user 15 | public let login: String 16 | /// The user's real name (or at least the name that the user 17 | /// has configured to be shown) or null if the user hasn't provided 18 | /// a real name for their public profile. 19 | public let name: String? 20 | /// The user's attributable email address or null if the 21 | /// user doesn't have an email address that they can be 22 | /// attributed by 23 | public let email: String 24 | /// A url to an avatar image chosen by the user 25 | public let avatar_url: String 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/IAPIRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIRepository.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | * Information about a repository as returned by the GitHub API. 12 | */ 13 | public struct IAPIRepository: Codable { 14 | let cloneUrl: String 15 | let sshUrl: String 16 | let htmlUrl: String 17 | let name: String 18 | let owner: IAPIIdentity 19 | let isPrivate: Bool 20 | let isFork: Bool 21 | let defaultBranch: String 22 | let pushedAt: String 23 | let hasIssues: Bool 24 | let isArchived: Bool 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/IAPIRepositoryCloneInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIRepositoryCloneInfo.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | // Define the IAPIRepositoryCloneInfo struct 11 | struct IAPIRepositoryCloneInfo { 12 | 13 | /** Canonical clone URL of the repository. */ 14 | let url: String 15 | 16 | /** 17 | * Default branch of the repository, if any. This is usually either retrieved 18 | * from the API for GitHub repositories, or undefined for other repositories. 19 | */ 20 | let defaultBranch: String? 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/IAPIRepositoryPermissions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIRepositoryPermissions.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /* 11 | * Information about how the user is permitted to interact with a repository. 12 | */ 13 | struct IAPIRepositoryPermissions: Codable { 14 | let admin: Bool 15 | let push: Bool 16 | let pull: Bool 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Issues/IAPIIssue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIIssue.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /** Information about an issue as returned by the GitHub API. */ 11 | struct IAPIIssue: Codable { 12 | let number: Int 13 | let title: String 14 | let state: APIIssueState 15 | let updatedAt: String 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Pull Request/IAPIComment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIComment.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents both issue comments and PR review comments. 12 | */ 13 | public struct IAPIComment: Codable { 14 | public let id: Int 15 | public let body: String 16 | public let html_url: String 17 | public let user: IAPIIdentity 18 | public let created_at: String 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Pull Request/IAPIPullRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIPullRequest.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents information about a pull request from the GitHub API. 12 | */ 13 | public struct IAPIPullRequest: Codable { 14 | public let number: Int 15 | public let title: String 16 | public let created_at: String 17 | public let updated_at: String 18 | public let user: IAPIIdentity 19 | public let head: IAPIPullRequestRef 20 | public let base: IAPIPullRequestRef 21 | public let body: String 22 | public let state: String 23 | public let draft: Bool? 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Pull Request/IAPIPullRequestRef.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIPullRequestRef.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents a pull request reference from the GitHub API. 12 | */ 13 | public struct IAPIPullRequestRef: Codable { 14 | public let ref: String 15 | public let sha: String 16 | public let repo: IAPIRepository? 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Pull Request/IAPIPullRequestReview.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents a pull request review from the GitHub API. 12 | */ 13 | public struct IAPIPullRequestReview: Codable { 14 | public let id: Int 15 | public let node_id: String 16 | public let user: IAPIIdentity 17 | public let body: String? 18 | public let commit_id: String 19 | public let submitted_at: String? 20 | public let state: APIPullRequestReviewState 21 | public let html_url: String 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Push/IAPIPushControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIPushControl.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | /** 9 | A structure representing information about push control settings for a protected branch. 10 | */ 11 | struct IAPIPushControl: Codable { 12 | /** 13 | * What status checks are required before merging? 14 | * 15 | * Empty array if user is admin and branch is not admin-enforced 16 | */ 17 | let required_status_checks: [String] 18 | 19 | /** 20 | * How many reviews are required before merging? 21 | * 22 | * 0 if user is admin and branch is not admin-enforced 23 | */ 24 | let required_approving_review_count: Int 25 | 26 | /** 27 | * Is user permitted? 28 | * 29 | * Always `true` for admins. 30 | * `true` if `Restrict who can push` is not enabled. 31 | * `true` if `Restrict who can push` is enabled and user is in list. 32 | * `false` if `Restrict who can push` is enabled and user is not in list. 33 | */ 34 | let allow_actor: Bool 35 | 36 | /** 37 | * Currently unused properties 38 | */ 39 | let pattern: String? 40 | let required_signatures: Bool 41 | let required_linear_history: Bool 42 | let allow_deletions: Bool 43 | let allow_force_pushes: Bool 44 | } 45 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Ruleset/IAPIRepoRule.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | /** 9 | Repository rule information returned by the GitHub API. 10 | */ 11 | public struct IAPIRepoRule: Codable { 12 | /** 13 | The ID of the ruleset this rule is configured in. 14 | */ 15 | public let ruleset_id: Int 16 | 17 | /** 18 | The type of the rule. 19 | */ 20 | public let type: APIRepoRuleType 21 | 22 | /** 23 | The parameters that apply to the rule if it is a metadata rule. 24 | Other rule types may have parameters, but they are not used in 25 | this app so they are ignored. Do not attempt to use this field 26 | unless you know `type` matches a metadata rule type. 27 | */ 28 | public let parameters: IAPIRepoRuleMetadataParameters? 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Ruleset/IAPIRepoRuleMetadataParameters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIRepoRuleMetadataParameters.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Metadata parameters for a repo rule metadata rule. 12 | */ 13 | public struct IAPIRepoRuleMetadataParameters: Codable { 14 | /** 15 | User-supplied name/description of the rule. 16 | */ 17 | public let name: String? 18 | 19 | /** 20 | Whether the operator is negated. For example, if `true` 21 | and `operator` is `starts_with`, then the rule 22 | will be negated to 'does not start with'. 23 | */ 24 | public let negate: Bool? 25 | 26 | /** 27 | The pattern to match against. If the operator is 'regex', then 28 | this is a regex string match. Otherwise, it is a raw string match 29 | of the type specified by `operator` with no additional parsing. 30 | */ 31 | public let pattern: String? 32 | 33 | /** 34 | The type of match to use for the pattern. For example, `starts_with` 35 | means `pattern` must be at the start of the string. 36 | */ 37 | public let `operator`: APIRepoRuleMetadataOperator? 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Ruleset/IAPIRepoRuleset.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIRepoRuleset.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum UserCanBypass: String, Codable { 11 | case always = "always" 12 | case pullRequestOnly = "pull_requests_only" 13 | case never = "never" 14 | } 15 | 16 | /** 17 | A ruleset returned from the GitHub API's "get a ruleset for a repo" endpoint. 18 | */ 19 | public struct IAPIRepoRuleset: Codable { 20 | /// The ID of the ruleset. 21 | public let id: Int 22 | 23 | /** 24 | Whether the user making the API request can bypass the ruleset. 25 | 26 | - Possible values: 27 | - `always`: The user can always bypass the ruleset. 28 | - `pull_requests_only`: The user can bypass the ruleset only for pull requests. 29 | - `never`: The user cannot bypass the ruleset. 30 | */ 31 | public let current_user_can_bypass: UserCanBypass 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Ruleset/IAPISlimRepoRuleset.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPISlimRepoRuleset.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | A ruleset returned from the GitHub API's "get all rulesets for a repo" endpoint. 12 | This endpoint returns a slimmed-down version of the full ruleset object, though 13 | only the ID is used. 14 | */ 15 | struct IAPISlimRepoRuleset: Codable { 16 | /// The ID of the ruleset. 17 | let id: Int 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Ruleset/IRawAPIRepoRule.swift: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Workflow/IAPICheckSuite.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPICheckSuite.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct IAPICheckSuite: Codable { 11 | let id: Int 12 | let rerequestable: Bool 13 | let runs_rerequestable: Bool 14 | let status: APICheckStatus 15 | let created_at: String 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Workflow/IAPIRefCheckRun.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIRefCheckRun.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct IAPIRefCheckRun: Codable { 11 | let id: Int 12 | let url: String 13 | let status: APICheckStatus 14 | let conclusion: APICheckConclusion? 15 | let name: String 16 | let check_suite: IAPIRefCheckRunCheckSuite 17 | let app: IAPIRefCheckRunApp 18 | let completed_at: String 19 | let started_at: String 20 | let html_url: String 21 | let pull_requests: [IAPIPullRequest] 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Workflow/IAPIRefCheckRunApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIRefCheckRunApp.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct IAPIRefCheckRunApp: Codable { 11 | let name: String 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Workflow/IAPIRefCheckRunCheckSuite.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIRefCheckRunCheckSuite.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct IAPIRefCheckRunCheckSuite: Codable { 11 | let id: Int 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Workflow/IAPIRefCheckRunOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIRefCheckRunOutput.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct IAPIRefCheckRunOutput: Codable { 11 | let title: String? 12 | let summary: String? 13 | let text: String? 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Workflow/IAPIRefCheckRuns.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIRefCheckRuns.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct IAPIRefCheckRuns: Codable { 11 | let total_count: Int 12 | let check_runs: [IAPIRefCheckRun] 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Workflow/IAPIRefStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIRefStatus.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct IAPIRefStatus: Codable { 11 | let state: APIRefState 12 | let total_count: Int 13 | let statuses: [IAPIRefStatusItem] 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Workflow/IAPIRefStatusItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIRefStatusItem.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct IAPIRefStatusItem: Codable { 11 | let state: APIRefState 12 | let target_url: String? 13 | let description: String 14 | let context: String 15 | let id: Int 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Workflow/IAPIWorkflowJob.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIWorkflowJob.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct IAPIWorkflowJob: Codable { 11 | let id: Int 12 | let name: String 13 | let status: APICheckStatus 14 | let conclusion: APICheckConclusion? 15 | let completed_at: String 16 | let started_at: String 17 | let steps: [IAPIWorkflowJobStep] 18 | let html_url: String 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Workflow/IAPIWorkflowJobStep.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIWorkflowJobStep.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct IAPIWorkflowJobStep: Codable { 11 | let name: String 12 | let number: Int 13 | let status: APICheckStatus 14 | let conclusion: APICheckConclusion? 15 | let completed_at: String 16 | let started_at: String 17 | let log: String 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Workflow/IAPIWorkflowJobs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIWorkflowJobs.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct IAPIWorkflowJobs: Codable { 11 | let total_count: Int 12 | let jobs: [IAPIWorkflowJob] 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Workflow/IAPIWorkflowRun.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIWorkflowRun.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct IAPIWorkflowRun: Codable { 11 | let id: Int 12 | let workflow_id: Int 13 | let cancel_url: String 14 | let created_at: String 15 | let logs_url: String 16 | let name: String 17 | let rerun_url: String 18 | let check_suite_id: Int 19 | let event: String 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Interfaces/Repo/Workflow/IAPIWorkflowRuns.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPIWorkflowRuns.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct IAPIWorkflowRuns: Codable { 11 | let total_count: Int 12 | let workflow_runs: [IAPIWorkflowRun] 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Model/Account/GithubAccount.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubAccount.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | // Define the Account class 11 | public class Account: Codable, Equatable { 12 | let login: String 13 | let endpoint: String 14 | let token: String 15 | let emails: [IAPIEmail] 16 | let avatarURL: String 17 | let id: Int 18 | let name: String 19 | let plan: String? 20 | 21 | public init(login: String, 22 | endpoint: String, 23 | token: String, 24 | emails: [IAPIEmail], 25 | avatarURL: String, 26 | id: Int, 27 | name: String, 28 | plan: String? 29 | ) { 30 | self.login = login 31 | self.endpoint = endpoint 32 | self.token = token 33 | self.emails = emails 34 | self.avatarURL = avatarURL 35 | self.id = id 36 | self.name = name 37 | self.plan = plan 38 | } 39 | 40 | func withToken(_ token: String) -> Account { 41 | return Account(login: self.login, 42 | endpoint: self.endpoint, 43 | token: token, 44 | emails: self.emails, 45 | avatarURL: self.avatarURL, 46 | id: self.id, 47 | name: self.name, 48 | plan: self.plan) 49 | } 50 | 51 | var friendlyName: String { 52 | return self.name.isEmpty ? self.login : self.name 53 | } 54 | 55 | public static func == (lhs: Account, rhs: Account) -> Bool { 56 | return lhs.endpoint == rhs.endpoint && lhs.id == rhs.id 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Model/Repo/Issues/APIIssueState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | enum APIIssueState: String, Codable { 11 | case open 12 | case closed 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Model/Repo/Pull Request/APIPullRequestReviewState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIPullRequestReviewState.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum APIPullRequestReviewState: String, Codable { 11 | case approved = "APPROVED" 12 | case dismissed = "DISMISSED" 13 | case pending = "PENDING" 14 | case commented = "COMMENTED" 15 | case changesRequested = "CHANGES_REQUESTED" 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Model/Repo/Ruleset/APIRepoRuleMetadataOperator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRepoRuleMetadataOperator.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Enum representing different operators for metadata rule matching. 12 | */ 13 | public enum APIRepoRuleMetadataOperator: String, Codable { 14 | case startsWith = "starts_with" 15 | case endsWith = "ends_with" 16 | case contains = "contains" 17 | case regex = "regex" 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Model/Repo/Ruleset/APIRepoRuleType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRepoRuleType.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Enum representing different types of repository rules that can be configured. 12 | */ 13 | public enum APIRepoRuleType: String, Codable { 14 | case creation 15 | case deletion 16 | case update 17 | case required_deployments 18 | case required_signatures 19 | case required_status_checks 20 | case required_linear_history 21 | case pull_request 22 | case commit_message_pattern 23 | case commit_author_email_pattern 24 | case committer_email_pattern 25 | case branch_name_pattern 26 | case non_fast_forward 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Model/Repo/Workflow/APICheckConclusion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APICheckConclusion.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | // The conclusion of a completed check run 11 | enum APICheckConclusion: String, Codable { 12 | case actionRequired = "action_required" 13 | case canceled 14 | case timedOut = "timed_out" 15 | case failure 16 | case neutral 17 | case success 18 | case skipped 19 | case stale 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Model/Repo/Workflow/APICheckStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APICheckStatus.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | // The overall status of a check run 11 | enum APICheckStatus: String, Codable { 12 | case queued 13 | case inProgress = "in_progress" 14 | case completed 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/GitHub/Model/Repo/Workflow/APIRefState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FiAPIRefStatele.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | // The combined state of a ref. 11 | enum APIRefState: String, Codable { 12 | case failure 13 | case pending 14 | case success 15 | case error 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/Gitlab/GitlabAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitlabAPI.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/10/29. 6 | // 7 | 8 | import Foundation 9 | 10 | struct GitlabAPI { 11 | 12 | public init() {} 13 | 14 | func createRepritory() { 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/API/Global/Gravatar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Gravatar.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/25. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Gravatar { 11 | 12 | /// Generates a Gravatar URL based on the provided email address and size. 13 | /// 14 | /// - Parameters: 15 | /// - email: The email address associated with the Gravatar. 16 | /// - size: An optional size parameter for the Gravatar image (default is 60). 17 | /// 18 | /// - Returns: A URL string representing the Gravatar image. 19 | /// 20 | /// - Example: 21 | /// ```swift 22 | /// let email = "example@example.com" 23 | /// let gravatarUrl = generateGravatarUrl(email: email, size: 80) 24 | /// ``` 25 | /// 26 | /// - Note: Gravatar is a service that provides globally recognized avatars associated with email addresses. 27 | func generateGravatarUrl(email: String, size: Int = 60) -> String { 28 | let hash = email.md5(trim: true, caseSensitive: false) 29 | return "https://www.gravatar.com/avatar/\(hash)?s=\(size)" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/Models/Github/Actions/Jobs/Job.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Job.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/09/13. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Job: Codable { 12 | public let totalCount: Int 13 | public let jobs: [Jobs] 14 | 15 | enum CodingKeys: String, CodingKey { 16 | case totalCount = "total_count" 17 | case jobs 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/Models/Github/Actions/Jobs/JobSteps.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JobSteps.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/09/13. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct JobSteps: Codable { 12 | public let name: String 13 | public let status: String 14 | public let conclusion: String 15 | public let number: Int 16 | public let startedAt: String 17 | public let completedAt: String 18 | 19 | enum CodingKeys: String, CodingKey { 20 | case name 21 | case status 22 | case conclusion 23 | case number 24 | case startedAt = "started_at" 25 | case completedAt = "completed_at" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/Models/Github/Actions/Jobs/Jobs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Jobs.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/09/13. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Jobs: Codable { 12 | public let id: Int 13 | public let runId: Int 14 | public let runURL: String 15 | public let runAttempt: Int 16 | public let url: String 17 | public let htmlURL: String 18 | public let status: String 19 | public let conclusion: String 20 | public let startedAt: String 21 | public let completedAt: String 22 | public let name: String 23 | public let steps: [JobSteps] 24 | public let runnerName: String? 25 | public let runnerGroupName: String? 26 | 27 | enum CodingKeys: String, CodingKey { 28 | case id 29 | case runId = "run_id" 30 | case runURL = "run_url" 31 | case runAttempt = "run_attempt" 32 | case url 33 | case htmlURL = "html_url" 34 | case status 35 | case conclusion 36 | case startedAt = "started_at" 37 | case completedAt = "completed_at" 38 | case name 39 | case steps 40 | case runnerName = "runner_name" 41 | case runnerGroupName = "runner_group_name" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/Models/Github/Actions/Workflow/Workflow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Workflow.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/09/13. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | public struct Workflow: Codable, Hashable, Identifiable, Comparable { 13 | public static func < (lhs: Workflow, rhs: Workflow) -> Bool { 14 | return lhs.name < rhs.name 15 | } 16 | 17 | public let id: Int 18 | public let nodeId: String 19 | public let name: String 20 | public let path: String 21 | public let state: String 22 | public let createdAt: String 23 | public let updatedAt: String 24 | public let url: String 25 | public let htmlURL: String 26 | 27 | enum CodingKeys: String, CodingKey { 28 | case id 29 | case nodeId = "node_id" 30 | case name 31 | case path 32 | case state 33 | case createdAt = "created_at" 34 | case updatedAt = "updated_at" 35 | case url 36 | case htmlURL = "html_url" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/Models/Github/Actions/Workflow/WorkflowRun.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkflowRun.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/09/13. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct WorkflowRun: Codable { 12 | public let id: Int 13 | public let name: String 14 | public let nodeId: String 15 | public let headBranch: String 16 | public let runNumber: Int 17 | public let status: String 18 | public let conclusion: String 19 | public let workflowId: Int 20 | public let url: String 21 | public let htmlURL: String 22 | public let createdAt: String 23 | public let updatedAt: String 24 | public let headCommit: WorkflowRunCommit 25 | 26 | enum CodingKeys: String, CodingKey { 27 | case id 28 | case name 29 | case nodeId = "node_id" 30 | case headBranch = "head_branch" 31 | case runNumber = "run_number" 32 | case status 33 | case conclusion 34 | case workflowId = "workflow_id" 35 | case url 36 | case htmlURL = "html_url" 37 | case createdAt = "created_at" 38 | case updatedAt = "updated_at" 39 | case headCommit = "head_commit" 40 | } 41 | } 42 | 43 | public struct WorkflowRunCommit: Codable { 44 | public let id: String 45 | public let treeId: String 46 | public let message: String 47 | public let timestamp: String 48 | public let author: CommitAuthor 49 | 50 | enum CodingKeys: String, CodingKey { 51 | case id 52 | case treeId = "tree_id" 53 | case message 54 | case timestamp 55 | case author 56 | } 57 | } 58 | 59 | public struct CommitAuthor: Codable { 60 | public let name: String 61 | public let email: String 62 | } 63 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/Models/Github/Actions/Workflow/WorkflowRuns.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkflowRuns.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/09/13. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct WorkflowRuns: Codable { 12 | public let totalCount: Int 13 | public let workflowRuns: [WorkflowRun]? 14 | 15 | enum CodingKeys: String, CodingKey { 16 | case totalCount = "total_count" 17 | case workflowRuns = "workflow_runs" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/Models/Github/Actions/Workflow/Workflows.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Workflows.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/09/13. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Workflows: Codable { 12 | public let totalCount: Int 13 | public let workflows: [Workflow] 14 | 15 | enum CodingKeys: String, CodingKey { 16 | case totalCount = "total_count" 17 | case workflows 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/Models/Github/Auth/2FA.swift: -------------------------------------------------------------------------------- 1 | // 2 | // 2FA.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | let authenticatorAppWelcomeText = 9 | "Please access the two-factor authentication application on your device in order to retrieve your authentication code and complete the identity verification process." 10 | // swiftlint:disable:previous line_length 11 | let smsMessageWelcomeText = 12 | "We have recently dispatched a message to you via SMS, containing your authentication code. Kindly input this code into the provided form below to authenticate your identity." 13 | // swiftlint:disable:previous line_length 14 | 15 | enum AuthenticationMode { 16 | /* 17 | * User should authenticate via a received text message. 18 | */ 19 | case sms 20 | /* 21 | * User should open TOTP mobile application and obtain code. 22 | */ 23 | case app 24 | } 25 | 26 | func getWelcomeMessage(type: AuthenticationMode) -> String { 27 | return type == .sms ? smsMessageWelcomeText : authenticatorAppWelcomeText 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/Networking/AuroraNetworkingConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuroraNetworkingConstants.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/26. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct AuroraNetworkingConstants { // swiftlint:disable:this convenience_type 11 | public static let GithubURL = "https://api.github.com/" 12 | public static let BitbucketURL = "https://api.bitbucket.org/2.0/" 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/Networking/AuroraNetworkingDebug.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuroraNetworkingDebug.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/26. 6 | // 7 | 8 | import Foundation 9 | 10 | extension AuroraNetworking { 11 | /// Return the full networkRequestResponse 12 | /// - Returns: the full networkRequestResponse 13 | public func networkRequestResponse() -> String? { 14 | return AuroraNetworking.fullResponse 15 | } 16 | 17 | func networkLog(request: URLRequest?, 18 | session: URLSession?, 19 | response: URLResponse?, 20 | data: Data?, 21 | file: String = #file, 22 | line: Int = #line, 23 | function: String = #function) { 24 | guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 else { 25 | return 26 | } 27 | 28 | #if DEBUG 29 | print("Network debug start") 30 | networkLogRequest(request) 31 | networkLogResponse(httpResponse) 32 | networkLogData(data) 33 | print("End of network debug\n") 34 | #endif 35 | } 36 | 37 | private func networkLogRequest(_ request: URLRequest?) { 38 | guard let request = request else { return } 39 | 40 | print("URLRequest:") 41 | if let httpMethod = request.httpMethod, let url = request.url { 42 | print(" \(httpMethod) \(url)") 43 | } 44 | 45 | print("\n Headers:") 46 | if let allHTTPHeaderFields = request.allHTTPHeaderFields { 47 | for (header, content) in allHTTPHeaderFields { 48 | print(" \(header): \(content)") 49 | } 50 | } 51 | 52 | print("\n Body:") 53 | if let httpBody = request.httpBody, let body = String(data: httpBody, encoding: .utf8) { 54 | print(" \(body)") 55 | } 56 | print("\n") 57 | } 58 | 59 | private func networkLogResponse(_ response: HTTPURLResponse) { 60 | print("HTTPURLResponse:") 61 | print(" HTTP \(response.statusCode)") 62 | 63 | for (header, content) in response.allHeaderFields { 64 | print(" \(header): \(content)") 65 | } 66 | } 67 | 68 | private func networkLogData(_ data: Data?) { 69 | guard let data = data, let stringData = String(data: data, encoding: .utf8) else { return } 70 | 71 | print("\n Body:") 72 | for line in stringData.split(separator: "\n") { 73 | print(" \(line)") 74 | } 75 | 76 | do { 77 | print("\n Decoded JSON:") 78 | if let jsonObject = try JSONSerialization.jsonObject(with: data) as? [String: Any] { 79 | for (key, value) in jsonObject { 80 | print(" \(key): \(value)") 81 | } 82 | } 83 | } catch { 84 | print("JSON Decoding Error: \(error.localizedDescription)") 85 | } 86 | } 87 | 88 | private func handleNetworkError(data: Data?) -> String { 89 | guard let data = data, 90 | let errorData = String(data: data, encoding: .utf8) else { 91 | return "Unknown error occurred." 92 | } 93 | 94 | return errorData 95 | .split(separator: "\n") 96 | .map(String.init) 97 | .joined(separator: " ") 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/Networking/HTTPErrors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPErros.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/09/13. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // 8 | 9 | /// A enum class that has strings that can be used to check what 10 | /// type of error we got back from the lookout api. 11 | enum HTTPErrors: String, Error { 12 | case notVerified = "User is not verified" 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Version-Control/Services/Networking/HTTPMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPMethod.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/09/13. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // 8 | 9 | public enum HTTPMethod: String { 10 | case GET, POST, PUT, PATCH, DELETE 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Version-Control/Utils/BranchUtil.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/05. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct BranchUtil { 11 | 12 | public init() {} 13 | 14 | /** 15 | Merges local and remote Git branches into a single array of Git \ 16 | branches that includes branches with upstream relationships. 17 | 18 | - Parameter branches: An array of `GitBranch` instances to be merged. 19 | - Returns: An array of `GitBranch` instances containing both local and \ 20 | remote branches with their respective upstream branches. 21 | 22 | This function takes an array of `GitBranch` instances and categorizes them into local and remote branches. 23 | It then creates a merged array that includes both types of branches along with their respective upstream branches. 24 | If a local branch has an associated upstream branch, it is included in the result. For remote branches, 25 | if the corresponding local branch is already added to the result, it is not added again to avoid duplication. 26 | */ 27 | public func mergeRemoteAndLocalBranches(branches: [GitBranch]) -> [GitBranch] { 28 | var localBranches = [GitBranch]() 29 | var remoteBranches = [GitBranch]() 30 | 31 | for branch in branches { 32 | if branch.type == .local { 33 | localBranches.append(branch) 34 | } else if branch.type == .remote { 35 | remoteBranches.append(branch) 36 | } 37 | } 38 | 39 | var upstreamBranchesAdded = Set() 40 | var allBranchesWithUpstream = [GitBranch]() 41 | 42 | for branch in localBranches { 43 | allBranchesWithUpstream.append(branch) 44 | 45 | if let upstream = branch.upstream { 46 | upstreamBranchesAdded.insert(upstream) 47 | } 48 | } 49 | 50 | for branch in remoteBranches { 51 | // This means we already added the local branch of this remote branch, so 52 | // we don't need to add it again. 53 | if upstreamBranchesAdded.contains(branch.name) { 54 | continue 55 | } 56 | 57 | allBranchesWithUpstream.append(branch) 58 | } 59 | 60 | return allBranchesWithUpstream 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/Version-Control/Utils/CommandError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommandError.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | enum CommandError: Error { 11 | case nonZeroExitStatus(Int) // Error with a non-zero exit status 12 | case utf8ConversionFailed // Error when UTF-8 conversion of output fails 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Version-Control/Utils/Extensions/Date.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2022/10/05. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Date { 11 | 12 | func yearMonthDayFormat() -> String { 13 | let dateFormatter = DateFormatter() 14 | dateFormatter.dateFormat = "yyyy-MM-dd" 15 | return dateFormatter.string(from: self) 16 | } 17 | 18 | func gitDateFormat(commitDate: String) -> Date? { 19 | let dateFormatter = DateFormatter() 20 | dateFormatter.locale = Locale.current 21 | dateFormatter.dateFormat = "E MMM dd HH:mm:ss yyyy Z" 22 | return dateFormatter.date(from: commitDate) 23 | } 24 | 25 | func toGitHubIsoDateString(_ date: Date) -> String { 26 | let dateFormatter = DateFormatter() 27 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" 28 | dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) 29 | return dateFormatter.string(from: date) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Version-Control/Utils/Extensions/FileManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileManger.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/16. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension FileManager { 12 | public func directoryExistsAtPath(_ path: String) -> Bool { 13 | var isDirectory: ObjCBool = true 14 | let exists = self.fileExists(atPath: "file://\(path)", isDirectory: &isDirectory) 15 | return exists && isDirectory.boolValue 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Version-Control/Utils/FileUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileUtils.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct FileUtils { 11 | 12 | func writeToTempFile(content: String, 13 | tempFileName: String) async throws -> String { 14 | let tempDir = NSTemporaryDirectory() 15 | let tempFilePath = (tempDir as NSString).appendingPathComponent(tempFileName) 16 | try content.write(toFile: tempFilePath, atomically: true, encoding: .utf8) 17 | return tempFilePath 18 | } 19 | 20 | func getOldPathOrDefault(file: FileChange) -> String { 21 | if file.status?.kind == .renamed || file.status?.kind == .copied { 22 | if let file = file.status as? CopiedOrRenamedFileStatus { 23 | return file.oldPath 24 | } else { 25 | return file.path 26 | } 27 | } else { 28 | return file.path 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Version-Control/Utils/Helpers/DefaultBranch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultBranch.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/13. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // This source code is restricted for Aurora Editor usage only. 8 | // 9 | 10 | import Foundation 11 | 12 | public struct DefaultBranch { 13 | 14 | /// The default branch name that GitHub Desktop will use when 15 | /// initializing a new repository. 16 | private let defaultBranchInAE = "main" 17 | 18 | /// The name of the Git configuration variable which holds what 19 | /// branch name Git will use when initializing a new repository. 20 | private let defaultBranchSettingName = "init.defaultBranch" 21 | 22 | /// The branch names that Aurora Editor shows by default as radio buttons on the 23 | /// form that allows users to change default branch name. 24 | public let suggestedBranchNames: [String] = ["main, master"] 25 | 26 | public init() {} 27 | 28 | /// Returns the configured default branch when creating new repositories 29 | public func getConfiguredDefaultBranch(path: URL) throws -> String? { 30 | // TODO: Bug where global config value is not being processed correctly 31 | return try Config().getGlobalConfigValue( 32 | path: path, 33 | name: defaultBranchSettingName 34 | ) 35 | } 36 | 37 | /// Returns the configured default branch when creating new repositories 38 | public func getDefaultBranch() -> String { 39 | // return try getConfiguredDefaultBranch() ?? defaultBranchInAE 40 | return defaultBranchInAE 41 | } 42 | 43 | /// Sets the configured default branch when creating new repositories. 44 | /// 45 | /// @param branchName - The default branch name to use. 46 | public func setDefaultBranch(branchName: String) throws -> String { 47 | return try Config().setGlobalConfigValue(name: defaultBranchSettingName, 48 | value: branchName) 49 | } 50 | 51 | public func findDefaultBranch(directoryURL: URL, 52 | branches: [GitBranch], 53 | defaultRemoteName: String?) throws -> GitBranch? { 54 | let remoteName: String? 55 | 56 | // TODO: Find a way to get upstream name 57 | remoteName = defaultRemoteName 58 | 59 | let remoteHead = remoteName != nil ? try Remote().getRemoteHEAD(directoryURL: directoryURL, 60 | remote: remoteName!) : nil 61 | 62 | let defaultBranchName = remoteHead ?? getDefaultBranch() 63 | let remoteRef = remoteHead != nil ? "\(remoteName!)/\(remoteHead!)" : nil 64 | 65 | var localHit: GitBranch? 66 | var localTrackingHit: GitBranch? 67 | var remoteHit: GitBranch? 68 | 69 | for branch in branches { 70 | if branch.type == .local { 71 | if branch.name == defaultBranchName { 72 | localHit = branch 73 | } 74 | 75 | if let remoteRef = remoteRef, branch.upstream == remoteRef { 76 | // Give preference to local branches that target the upstream 77 | // default branch that also match the name. In other words, if there 78 | // are two local branches which both track the origin default branch 79 | // we'll prefer a branch which is also named the same as the default 80 | // branch name. 81 | if localTrackingHit == nil || branch.name == defaultBranchName { 82 | localTrackingHit = branch 83 | } 84 | } 85 | } else if let remoteRef = remoteRef, branch.name == remoteRef { 86 | remoteHit = branch 87 | } 88 | } 89 | 90 | // When determining what the default branch is we give priority to local 91 | // branches tracking the default branch of the contribution target (think 92 | // origin) remote, then we consider local branches that are named the same 93 | // as the default branch, and finally we look for the remote branch 94 | // representing the default branch of the contribution target 95 | return localTrackingHit ?? localHit ?? remoteHit 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /Sources/Version-Control/Utils/Helpers/GitAuthor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitAuthor.swift 3 | // AuroraEditor 4 | // 5 | // Created by Nanashi Li on 2022/08/15. 6 | // Copyright © 2022 Aurora Company. All rights reserved. 7 | // This source code is restricted for Aurora Editor usage only. 8 | // 9 | 10 | import Foundation 11 | 12 | public struct GitAuthor: Codable, Hashable, Equatable { 13 | public var name: String 14 | public var email: String 15 | 16 | public init(name: String?, email: String?) { 17 | self.name = name ?? "Unknown" 18 | self.email = email ?? "Unknown" 19 | } 20 | 21 | public func parse(nameAddr: String) -> GitAuthor? { 22 | let value = nameAddr.components(separatedBy: "/^(.*?)\\s+<(.*?)>//") 23 | return value.isEmpty ? nil : GitAuthor(name: value[1], 24 | email: value[2]) 25 | } 26 | 27 | public func toString() -> String { 28 | return "\(self.name) \(self.email)" 29 | } 30 | 31 | public static func == (lhs: GitAuthor, rhs: GitAuthor) -> Bool { 32 | lhs.email == rhs.email 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Version-Control/Utils/Helpers/MediaDiff.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct MediaDiff { 11 | 12 | public init() {} 13 | 14 | /// Returns the media type of a file as a string based on its file extension. 15 | /// 16 | /// The function compares the file extension to a set of known image file types 17 | /// and returns the corresponding media type. If the file extension is not recognized 18 | /// as one of the predefined image types, the function defaults to returning "text/plain". 19 | /// 20 | /// - Parameter extension: A string representing the file extension. 21 | /// - Returns: A string representing the media type of the file. 22 | /// 23 | /// # Example: 24 | /// ``` 25 | /// let mediaType = getMediaType(extension: ".png") // Returns "image/png" 26 | /// ``` 27 | /// 28 | /// - Note: This function currently supports the following image media types: 29 | /// - PNG (.png) 30 | /// - JPEG (.jpg, .jpeg) 31 | /// - GIF (.gif) 32 | /// - ICO (.ico) 33 | /// - WEBP (.webp) 34 | /// - BMP (.bmp) 35 | /// - AVIF (.avif) 36 | func getMediaType(extension: String) -> String { 37 | if `extension` == ".png" { 38 | return "image/png" 39 | } 40 | if `extension` == ".jpg" || `extension` == ".jpeg" { 41 | return "image/jpg" 42 | } 43 | if `extension` == ".gif" { 44 | return "image/gif" 45 | } 46 | if `extension` == ".ico" { 47 | return "image/x-icon" 48 | } 49 | if `extension` == ".webp" { 50 | return "image/webp" 51 | } 52 | if `extension` == ".bmp" { 53 | return "image/bmp" 54 | } 55 | if `extension` == ".avif" { 56 | return "image/avif" 57 | } 58 | 59 | return "text/plain" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/Version-Control/Utils/Helpers/Regex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Regex.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/10/29. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | This class provides methods to find and extract regex matches and their captured groups 12 | from a given text using NSRegularExpression. 13 | */ 14 | class Regex { 15 | 16 | init() {} 17 | 18 | /** 19 | Get captured groups from regex matches within a given text. 20 | 21 | - Parameters: 22 | - text: The input string to search for matches and captures. 23 | - expression: The regular expression to use for matching. It should have the global option. 24 | 25 | - Returns: An array of arrays of strings representing the captured groups from each match. \ 26 | The outer array contains one element for each match, and the inner arrays contain the captured strings. 27 | */ 28 | func getCaptures(text: String, expression: NSRegularExpression) -> [[String]] { 29 | let matches = getMatches(text: text, expression: expression) 30 | var captures: [[String]] = [] 31 | 32 | for match in matches { 33 | let capturedStrings = (1.. [NSTextCheckingResult] { 52 | let range = NSRange(text.startIndex..., in: text) 53 | let matches = expression.matches(in: text, range: range) 54 | return matches 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/Version-Control/Utils/Helpers/RemoveRemotePrefix.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RemoveRemotePrefix.swift 3 | // 4 | // 5 | // Created by Nanashi Li on 2023/09/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Remove the remote prefix from a branch name. 11 | /// 12 | /// If a branch name includes a remote prefix, \ 13 | /// this function extracts the branch name itself by removing the remote prefix. \ 14 | /// If no prefix is found, it returns `nil`. 15 | /// 16 | /// - Parameter name: The branch name that may include a remote prefix. 17 | /// 18 | /// - Returns: The branch name without the remote prefix, or `nil` if no remote prefix is present in the input name. 19 | /// 20 | /// - Example: 21 | /// ```swift 22 | /// let branchName = "origin/main" // Replace with the branch name 23 | /// let extractedBranch = removeRemotePrefix(name: branchName) 24 | /// if let branch = extractedBranch { 25 | /// print("Extracted Branch: \(branch)") 26 | /// } else { 27 | /// print("No remote prefix found.") 28 | /// } 29 | /// ``` 30 | /// 31 | /// - Note: 32 | /// The remote prefix typically includes the name of the remote repository and a forward slash (`/`). \ 33 | /// This function is useful for extracting the local branch name from a branch name that includes the remote prefix. 34 | /// 35 | /// - Warning: 36 | /// Ensure that the input `name` is a valid branch name or includes a remote prefix to avoid unexpected results. 37 | /// 38 | /// - Returns: The extracted branch name or `nil` if no remote prefix is present in the input name. 39 | func removeRemotePrefix(name: String) -> String? { 40 | let regexPattern = #".*?/(.*)"# 41 | 42 | if let regex = try? NSRegularExpression(pattern: regexPattern, options: []) { 43 | if let match = regex.firstMatch(in: name, options: [], range: NSRange(location: 0, length: name.utf16.count)) { 44 | let remoteBranch = (name as NSString).substring(with: match.range(at: 1)) 45 | return remoteBranch 46 | } 47 | } 48 | 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Version-Control/Utils/LiveShellClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // shellClient.swift 3 | // AuroraEditor 4 | // 5 | // Created by Wesley de Groot on 22/07/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public var sharedShellClient: LiveShellClient = .init() 11 | 12 | // Inspired by: https://vimeo.com/291588126 13 | public struct LiveShellClient { 14 | public var shellClient: ShellClient = .live() 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Version-Control/Version_Control.swift: -------------------------------------------------------------------------------- 1 | public struct VersionControl { 2 | public private(set) var text = "Hello, World!" 3 | 4 | public init() { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/Version-Control-Test/Services/API/GitHub/Mock Data/GitHubAccountResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "octocat", 3 | "id": 1, 4 | "node_id": "MDQ6VXNlcjE=", 5 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 6 | "gravatar_id": "", 7 | "url": "https://api.github.com/users/octocat", 8 | "html_url": "https://github.com/octocat", 9 | "followers_url": "https://api.github.com/users/octocat/followers", 10 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 11 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 12 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 13 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 14 | "organizations_url": "https://api.github.com/users/octocat/orgs", 15 | "repos_url": "https://api.github.com/users/octocat/repos", 16 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 17 | "received_events_url": "https://api.github.com/users/octocat/received_events", 18 | "type": "User", 19 | "site_admin": false, 20 | "name": "monalisa octocat", 21 | "company": "GitHub", 22 | "blog": "https://github.com/blog", 23 | "location": "San Francisco", 24 | "email": "octocat@github.com", 25 | "hireable": false, 26 | "bio": "There once was...", 27 | "twitter_username": "monatheoctocat", 28 | "public_repos": 2, 29 | "public_gists": 1, 30 | "followers": 20, 31 | "following": 0, 32 | "created_at": "2008-01-14T04:33:35Z", 33 | "updated_at": "2008-01-14T04:33:35Z", 34 | "private_gists": 81, 35 | "total_private_repos": 100, 36 | "owned_private_repos": 100, 37 | "disk_usage": 10000, 38 | "collaborators": 8, 39 | "two_factor_authentication": true, 40 | "plan": { 41 | "name": "Medium", 42 | "space": 400, 43 | "private_repos": 20, 44 | "collaborators": 0 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/Version-Control-Test/Services/API/GitHub/Mock Data/ProtectedBranchesResponse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "development", 4 | "commit": { 5 | "sha": "c293badc0c44ad4e78ebd2a741731709d8c58d18", 6 | "url": "https://api.github.com/repos/AuroraEditor/AuroraEditor/commits/c293badc0c44ad4e78ebd2a741731709d8c58d18" 7 | }, 8 | "protected": true 9 | }, 10 | { 11 | "name": "main", 12 | "commit": { 13 | "sha": "7f7b465a3945af5717ef6d9faee57647ac8de79a", 14 | "url": "https://api.github.com/repos/AuroraEditor/AuroraEditor/commits/7f7b465a3945af5717ef6d9faee57647ac8de79a" 15 | }, 16 | "protected": true 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /Tests/Version-Control-Test/Services/API/GitHub/Mock Data/PushControlResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "development", 3 | "commit": { 4 | "sha": "c293badc0c44ad4e78ebd2a741731709d8c58d18", 5 | "url": "https://api.github.com/repos/AuroraEditor/AuroraEditor/commits/c293badc0c44ad4e78ebd2a741731709d8c58d18" 6 | }, 7 | "protected": true, 8 | "pattern": "development", 9 | "required_signatures": false, 10 | "required_status_checks": [], 11 | "required_approving_review_count": 0, 12 | "required_linear_history": false, 13 | "allow_actor": true, 14 | "allow_deletions": false, 15 | "allow_force_pushes": false, 16 | "block_creations": false 17 | } 18 | --------------------------------------------------------------------------------