├── .github └── workflows │ ├── docc.yml │ └── swift.yml ├── .gitignore ├── .spi.yml ├── .swiftformat ├── .swiftlint.yml ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ ├── xcbaselines │ └── SwiftGitXTests.xcbaseline │ │ └── Info.plist │ └── xcschemes │ └── SwiftGitX.xcscheme ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── SwiftGitX │ ├── Collections │ ├── BranchCollection.swift │ ├── ConfigCollection.swift │ ├── IndexCollection.swift │ ├── ReferenceCollection.swift │ ├── RemoteCollection.swift │ ├── StashCollection.swift │ └── TagCollection.swift │ ├── Helpers │ ├── Constants.swift │ ├── Extensions │ │ ├── Array+withGitStrArray.swift │ │ └── URL+relativePath.swift │ ├── Factory │ │ ├── ObjectFactory.swift │ │ └── ReferenceFactory.swift │ └── LibGit2RawRepresentable.swift │ ├── Models │ ├── Diff │ │ ├── Diff.swift │ │ ├── Patch.swift │ │ └── StatusEntry.swift │ ├── FileMode.swift │ ├── Options │ │ ├── CheckoutOptions.swift │ │ ├── CloneOptions.swift │ │ ├── DiffOption.swift │ │ ├── LogSortingOption.swift │ │ ├── ResetOption.swift │ │ ├── RestoreOption.swift │ │ ├── StashOption.swift │ │ └── StatusOption.swift │ ├── Progress │ │ ├── CheckoutProgress.swift │ │ └── TransferProgress.swift │ ├── Remote.swift │ ├── Signature.swift │ ├── StashEntry.swift │ └── Types │ │ ├── BranchType.swift │ │ ├── ObjectType.swift │ │ └── TagType.swift │ ├── Objects │ ├── Blob.swift │ ├── Commit.swift │ ├── OID.swift │ ├── Object.swift │ └── Tree.swift │ ├── References │ ├── Branch.swift │ ├── Reference.swift │ └── Tag.swift │ ├── Repository.swift │ ├── Sequences │ ├── BranchSequence.swift │ ├── CommitSequence.swift │ ├── ReferenceIterator.swift │ ├── RemoteIterator.swift │ ├── StashIterator.swift │ └── TagIterator.swift │ ├── SwiftGitX.docc │ ├── Articles │ │ └── GettingStarted.md │ ├── Extensions │ │ ├── Repository.md │ │ └── SwiftGitX-Extension.md │ ├── Hierarchy │ │ ├── Collections.md │ │ ├── ErrorTypes.md │ │ ├── GitModels.md │ │ ├── HelperModels.md │ │ ├── Iterators.md │ │ ├── Objects.md │ │ └── References.md │ ├── Resources │ │ └── Git-Icon-White.png │ └── SwiftGitX.md │ └── SwiftGitX.swift └── Tests ├── SwiftGitX.xctestplan └── SwiftGitXTests ├── CollectionTests ├── BranchCollectionTests.swift ├── ConfigCollection.swift ├── IndexCollectionTests.swift ├── ReferenceCollectionTests.swift ├── RemoteCollectionTests.swift ├── StashCollectionTests.swift └── TagCollectionTests.swift ├── ObjectTests.swift ├── PerformanceTests └── RepositoryPerformanceTests.swift ├── RepositoryTests ├── RepositoryDiffTests.swift ├── RepositoryOperationTests.swift ├── RepositoryPropertyTests.swift ├── RepositoryRemoteOperationTests.swift ├── RepositoryShowTests.swift ├── RepositorySwitchTests.swift └── RepositoryTests.swift └── SwiftGitXTests.swift /.github/workflows/docc.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Build and Deploy Swift-DocC Documentation 3 | 4 | on: 5 | # Runs on releases targeting the default branch 6 | release: 7 | types: [published] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | build: 26 | runs-on: macos-latest 27 | 28 | environment: 29 | name: github-pages 30 | url: ${{ steps.deployment.outputs.page_url }} 31 | # This job runs on the ubuntu-latest runner 32 | 33 | steps: 34 | # Generates the documentation using Swift-DocC 35 | - name: Checkout 36 | uses: actions/checkout@v4 37 | - name: Generate documentation 38 | run: > 39 | swift package --allow-writing-to-directory .docs 40 | generate-documentation --target SwiftGitX 41 | --disable-indexing 42 | --transform-for-static-hosting 43 | --hosting-base-path SwiftGitX 44 | --output-path .docs 45 | 46 | # Deploy the generated documentation to GitHub Pages 47 | - name: Setup Pages 48 | uses: actions/configure-pages@v5 49 | - name: Upload artifact 50 | uses: actions/upload-pages-artifact@v3 51 | with: 52 | # Upload .docs directory 53 | path: '.docs' 54 | - name: Deploy to GitHub Pages 55 | id: deployment 56 | uses: actions/deploy-pages@v4 57 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Swift project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 3 | 4 | name: Swift Build and Test 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | runs-on: macos-latest 15 | 16 | steps: 17 | # Checkout the code 18 | - uses: actions/checkout@v4 19 | # Cache the Swift Package Manager build directory 20 | - uses: actions/cache@v3 21 | with: 22 | path: .build 23 | key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} 24 | # Resolve dependencies 25 | - name: Resolve dependencies 26 | run: swift package resolve 27 | # Clean up the build directory 28 | - name: Clean up 29 | run: swift package clean 30 | # Build the project 31 | - name: Build SwiftGitX 32 | run: swift build -v 33 | # Update git configurations 34 | - name: Update git configurations 35 | run: > 36 | git config --global init.defaultBranch main && 37 | git config --global user.name "İbrahim Çetin" && 38 | git config --global user.email "mail@ibrahimcetin.dev" 39 | # Run tests 40 | - name: Run tests 41 | run: swift test -v 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | .vscode/ -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [SwiftGitX] 5 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --commas inline 2 | --maxwidth 120 3 | --wrapparameters before-first 4 | --swiftversion 5.10 5 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | excluded: # paths to ignore during linting. Takes precedence over `included`. 2 | - Pods 3 | - R.generated.swift 4 | - .build # Where Swift Package Manager checks out dependency sources 5 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcbaselines/SwiftGitXTests.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | 278365DB-C8F3-4780-A259-F78F226C1ED1 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 0 13 | cpuCount 14 | 1 15 | cpuKind 16 | Apple M1 Pro 17 | cpuSpeedInMHz 18 | 0 19 | logicalCPUCoresPerPackage 20 | 8 21 | modelCode 22 | MacBookPro18,3 23 | physicalCPUCoresPerPackage 24 | 8 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | arm64e 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/SwiftGitX.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 35 | 36 | 37 | 38 | 41 | 47 | 48 | 49 | 50 | 51 | 61 | 62 | 68 | 69 | 75 | 76 | 77 | 78 | 80 | 81 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 İbrahim Çetin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "collectionconcurrencykit", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/JohnSundell/CollectionConcurrencyKit.git", 7 | "state" : { 8 | "revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95", 9 | "version" : "0.2.0" 10 | } 11 | }, 12 | { 13 | "identity" : "cryptoswift", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", 16 | "state" : { 17 | "revision" : "c9c3df6ab812de32bae61fc0cd1bf6d45170ebf0", 18 | "version" : "1.8.2" 19 | } 20 | }, 21 | { 22 | "identity" : "libgit2", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/ibrahimcetin/libgit2.git", 25 | "state" : { 26 | "revision" : "6e627a8d345d9de7df6f69c134ec5dfa81e75ade", 27 | "version" : "1.8.0" 28 | } 29 | }, 30 | { 31 | "identity" : "sourcekitten", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/jpsim/SourceKitten.git", 34 | "state" : { 35 | "revision" : "fd4df99170f5e9d7cf9aa8312aa8506e0e7a44e7", 36 | "version" : "0.35.0" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-argument-parser", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/apple/swift-argument-parser.git", 43 | "state" : { 44 | "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", 45 | "version" : "1.4.0" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-docc-plugin", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/apple/swift-docc-plugin", 52 | "state" : { 53 | "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", 54 | "version" : "1.3.0" 55 | } 56 | }, 57 | { 58 | "identity" : "swift-docc-symbolkit", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/apple/swift-docc-symbolkit", 61 | "state" : { 62 | "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", 63 | "version" : "1.0.0" 64 | } 65 | }, 66 | { 67 | "identity" : "swift-syntax", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/apple/swift-syntax.git", 70 | "state" : { 71 | "revision" : "303e5c5c36d6a558407d364878df131c3546fad8", 72 | "version" : "510.0.2" 73 | } 74 | }, 75 | { 76 | "identity" : "swiftformat", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/nicklockwood/SwiftFormat.git", 79 | "state" : { 80 | "revision" : "dd989a46d0c6f15c016484bab8afe5e7a67a4022", 81 | "version" : "0.54.0" 82 | } 83 | }, 84 | { 85 | "identity" : "swiftlint", 86 | "kind" : "remoteSourceControl", 87 | "location" : "https://github.com/realm/SwiftLint.git", 88 | "state" : { 89 | "revision" : "b515723b16eba33f15c4677ee65f3fef2ce8c255", 90 | "version" : "0.55.1" 91 | } 92 | }, 93 | { 94 | "identity" : "swiftytexttable", 95 | "kind" : "remoteSourceControl", 96 | "location" : "https://github.com/scottrhoyt/SwiftyTextTable.git", 97 | "state" : { 98 | "revision" : "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3", 99 | "version" : "0.9.0" 100 | } 101 | }, 102 | { 103 | "identity" : "swxmlhash", 104 | "kind" : "remoteSourceControl", 105 | "location" : "https://github.com/drmohundro/SWXMLHash.git", 106 | "state" : { 107 | "revision" : "a853604c9e9a83ad9954c7e3d2a565273982471f", 108 | "version" : "7.0.2" 109 | } 110 | }, 111 | { 112 | "identity" : "yams", 113 | "kind" : "remoteSourceControl", 114 | "location" : "https://github.com/jpsim/Yams.git", 115 | "state" : { 116 | "revision" : "9234124cff5e22e178988c18d8b95a8ae8007f76", 117 | "version" : "5.1.2" 118 | } 119 | } 120 | ], 121 | "version" : 2 122 | } 123 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 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: "SwiftGitX", 8 | platforms: [ 9 | .macOS(.v11), 10 | .iOS(.v13) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, making them visible to other packages. 14 | .library( 15 | name: "SwiftGitX", 16 | targets: ["SwiftGitX"] 17 | ) 18 | ], 19 | dependencies: [ 20 | .package(url: "https://github.com/ibrahimcetin/libgit2.git", from: "1.8.0"), 21 | .package(url: "https://github.com/realm/SwiftLint.git", from: "0.54.0"), 22 | .package(url: "https://github.com/nicklockwood/SwiftFormat.git", from: "0.53.0"), 23 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0") 24 | ], 25 | targets: [ 26 | // Targets are the basic building blocks of a package, defining a module or a test suite. 27 | // Targets can depend on other targets in this package and products from dependencies. 28 | .target( 29 | name: "SwiftGitX", 30 | dependencies: ["libgit2"] 31 | ), 32 | .testTarget( 33 | name: "SwiftGitXTests", 34 | dependencies: ["SwiftGitX"] 35 | ) 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftGitX 2 | 3 | Welcome to SwiftGitX! 🎉 4 | 5 | SwiftGitX is a modern Swift wrapper for [libgit2](https://libgit2.org). It's designed to make working with Git in Swift easy and efficient. Plus, it fully supports the [Swift Package Manager](https://github.com/swiftlang/swift-package-manager) and has no external dependencies. 6 | 7 | ```swift 8 | let url = URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! 9 | let repository = try await Repository.clone(from: url, to: URL(string: "/path/to/clone")!) 10 | 11 | let latestCommit = try repository.HEAD.target as? Commit 12 | 13 | let main = try repository.branch.get(named: "main") 14 | let feature = try repository.branch.create(named: "feature", from: main) 15 | try repository.switch(to: feature) 16 | ``` 17 | 18 | ## Why Choose SwiftGitX? 19 | 20 | SwiftGitX offers: 21 | 22 | - **Swift concurrency support**: Take advantage of async/await for smooth, non-blocking Git operations. 23 | - **Throwing functions**: Handle errors gracefully with Swift's error handling. 24 | - **Full SPM support**: Easily integrate SwiftGitX into your projects. 25 | - **Intuitive design**: A user-friendly API that's similar to the Git command line interface, making it easy to learn and use. 26 | - **Wrapper, not just bindings**: SwiftGitX provides a complete Swift experience with no low-level C functions or types. It also includes modern Git commands, offering more functionality than other libraries. 27 | 28 | ## Adding SwiftGitX to Your Project 29 | 30 | To get started, just add SwiftGitX to your project: 31 | 32 | 1. File > Add Package Dependencies... 33 | 2. Enter https://github.com/ibrahimcetin/SwiftGitX.git 34 | 3. Select "Up to Next Major" with "0.1.0" 35 | 36 | Or add SwiftGitX to your `Package.swift` file: 37 | 38 | ```swift 39 | dependencies: [ 40 | .package(url: "https://github.com/ibrahimcetin/SwiftGitX.git", from: "0.1.0"), 41 | ] 42 | ``` 43 | 44 | ## Documentation 45 | 46 | You can access the documentation in three ways: 47 | 48 | - **Online Documentation** – Based on the most recent tagged release: [View here](https://ibrahimcetin.github.io/SwiftGitX/documentation/swiftgitx/). 49 | - **Xcode Documetation** - Thanks to [Swift-Docc](https://www.swift.org/documentation/docc/), you can access everything seamlessly in Xcode. 50 | - **Upstream Documentation** – Reflecting the latest changes from the main branch: [View here](https://swiftpackageindex.com/ibrahimcetin/SwiftGitX/main/documentation/swiftgitx). 51 | 52 | ## Building and Testing 53 | 54 | SwiftGitX is easy to build and test. It requires only Swift, no additional system dependencies. 55 | To build SwiftGitX, run: 56 | ```bash 57 | swift build 58 | ``` 59 | To test SwiftGitX, run: 60 | ```bash 61 | swift test 62 | ``` 63 | 64 | ## Contributing 65 | 66 | We welcome contributions! Whether you want to report a bug, request a feature, improve documentation, or add tests, we appreciate your help. 67 | 68 | **For developers**, when contributing, please ensure to add appropriate tests and documentation to keep our project robust and well-documented. 69 | 70 | --- 71 | 72 | Thank you for considering SwiftGitX for your project. I'm excited to see what you’ll build! 😊 73 | 74 | --- 75 | 76 | Feel free to let me know if there's anything specific you'd like to adjust further! 🚀 77 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Collections/ConfigCollection.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | // ? Should we use actor? 4 | /// A collection of configurations and their operations. 5 | public struct ConfigCollection { 6 | private let repositoryPointer: OpaquePointer? 7 | 8 | /// Init for repository configurations. 9 | init(repositoryPointer: OpaquePointer) { 10 | self.repositoryPointer = repositoryPointer 11 | } 12 | 13 | /// Init for global configurations. 14 | init() { 15 | repositoryPointer = nil 16 | } 17 | 18 | /// The default branch name of the repository 19 | /// 20 | /// - Returns: The default branch name of the repository 21 | /// 22 | /// This is the branch that is checked out when the repository is initialized. 23 | public var defaultBranchName: String { 24 | var configPointer: OpaquePointer? 25 | defer { git_config_free(configPointer) } 26 | 27 | if let repositoryPointer { 28 | git_repository_config(&configPointer, repositoryPointer) 29 | } else { 30 | git_config_open_default(&configPointer) 31 | } 32 | 33 | var branchNameBuffer = git_buf() 34 | defer { git_buf_free(&branchNameBuffer) } 35 | 36 | git_config_get_string_buf(&branchNameBuffer, configPointer, "init.defaultBranch") 37 | 38 | return String(cString: branchNameBuffer.ptr) 39 | } 40 | 41 | /// Sets a configuration value for the repository. 42 | /// 43 | /// - Parameters: 44 | /// - string: The value to set. 45 | /// - key: The key to set the value for. 46 | /// 47 | /// This will set the configuration value for the repository. 48 | public func set(_ string: String, forKey key: String) { 49 | var configPointer: OpaquePointer? 50 | defer { git_config_free(configPointer) } 51 | 52 | if let repositoryPointer { 53 | git_repository_config(&configPointer, repositoryPointer) 54 | } else { 55 | git_config_open_default(&configPointer) 56 | } 57 | 58 | guard let configPointer else { 59 | // TODO: Handle error 60 | return 61 | } 62 | 63 | git_config_set_string(configPointer, key, string) 64 | } 65 | 66 | /// Returns the configuration value for the repository. 67 | /// 68 | /// - Parameter key: The key to get the value for. 69 | /// 70 | /// - Returns: The configuration value for the key. 71 | /// 72 | /// All config files will be looked into, in the order of their defined level. A higher level means a higher 73 | /// priority. The first occurrence of the variable will be returned here. 74 | public func string(forKey key: String) -> String? { 75 | var configPointer: OpaquePointer? 76 | defer { git_config_free(configPointer) } 77 | 78 | if let repositoryPointer { 79 | git_repository_config(&configPointer, repositoryPointer) 80 | } else { 81 | git_config_open_default(&configPointer) 82 | } 83 | 84 | guard let configPointer else { 85 | // TODO: Handle error 86 | return nil 87 | } 88 | 89 | var valueBuffer = git_buf() 90 | defer { git_buf_free(&valueBuffer) } 91 | 92 | let status = git_config_get_string_buf(&valueBuffer, configPointer, key) 93 | 94 | guard let pointer = valueBuffer.ptr, status == GIT_OK.rawValue else { 95 | return nil 96 | } 97 | 98 | return String(cString: pointer) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Collections/IndexCollection.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import libgit2 3 | 4 | /// An error that occurred while performing an index operation. 5 | public enum IndexError: Error { 6 | /// An error occurred while reading the index from the repository. 7 | case failedToReadIndex(String) 8 | 9 | /// An error occurred while writing the index back to the repository. 10 | case failedToWriteIndex(String) 11 | 12 | /// An error occurred while adding a file to the index. 13 | case failedToAddFile(String) 14 | 15 | /// An error occurred while removing a file from the index. 16 | case failedToRemoveFile(String) 17 | } 18 | 19 | /// A collection of index operations. 20 | struct IndexCollection { 21 | private let repositoryPointer: OpaquePointer 22 | 23 | init(repositoryPointer: OpaquePointer) { 24 | self.repositoryPointer = repositoryPointer 25 | } 26 | 27 | /// The error message from the last failed operation. 28 | private var errorMessage: String { 29 | String(cString: git_error_last().pointee.message) 30 | } 31 | 32 | /// Reads the index from the repository. 33 | /// 34 | /// - Returns: The index pointer. 35 | /// 36 | /// The returned index pointer must be freed using `git_index_free`. 37 | private func readIndexPointer() throws -> OpaquePointer { 38 | var indexPointer: OpaquePointer? 39 | let status = git_repository_index(&indexPointer, repositoryPointer) 40 | 41 | guard let indexPointer, status == GIT_OK.rawValue else { 42 | throw IndexError.failedToReadIndex(errorMessage) 43 | } 44 | 45 | return indexPointer 46 | } 47 | 48 | /// Writes the index back to the repository. 49 | /// 50 | /// - Parameter indexPointer: The index pointer. 51 | private func writeIndex(indexPointer: OpaquePointer) throws { 52 | let status = git_index_write(indexPointer) 53 | 54 | guard status == GIT_OK.rawValue else { 55 | throw IndexError.failedToWriteIndex(errorMessage) 56 | } 57 | } 58 | 59 | /// Returns the repository working directory relative path for a file. 60 | /// 61 | /// - Parameter file: The file URL. 62 | private func relativePath(for file: URL) throws -> String { 63 | guard let rawWorkingDirectory = git_repository_workdir(repositoryPointer) else { 64 | throw RepositoryError.failedToGetWorkingDirectory 65 | } 66 | 67 | let workingDirectory = URL(fileURLWithPath: String(cString: rawWorkingDirectory), isDirectory: true) 68 | 69 | return try file.relativePath(from: workingDirectory) 70 | } 71 | 72 | /// Adds a file to the index. 73 | /// 74 | /// - Parameter path: The file path relative to the repository root directory. 75 | /// 76 | /// The path should be relative to the repository root directory. 77 | /// For example, `README.md` or `Sources/SwiftGitX/Repository.swift`. 78 | func add(path: String) throws { 79 | // Read the index 80 | let indexPointer = try readIndexPointer() 81 | defer { git_index_free(indexPointer) } 82 | 83 | // Add the file to the index 84 | let status = git_index_add_bypath(indexPointer, path) 85 | 86 | guard status == GIT_OK.rawValue else { 87 | throw IndexError.failedToAddFile(errorMessage) 88 | } 89 | 90 | // Write the index back to the repository 91 | try writeIndex(indexPointer: indexPointer) 92 | } 93 | 94 | /// Adds a file to the index. 95 | /// 96 | /// - Parameter file: The file URL. 97 | /// 98 | /// The file should be a URL to a file in the repository. 99 | func add(file: URL) throws { 100 | // Get the relative path of the file 101 | let relativePath = try relativePath(for: file) 102 | 103 | // Add the file to the index 104 | try add(path: relativePath) 105 | } 106 | 107 | /// Adds files to the index. 108 | /// 109 | /// - Parameter paths: The file paths relative to the repository root directory. 110 | /// 111 | /// The paths should be relative to the repository root directory. 112 | /// For example, `README.md` or `Sources/SwiftGitX/Repository.swift`. 113 | func add(paths: [String]) throws { 114 | // Read the index 115 | let indexPointer = try readIndexPointer() 116 | defer { git_index_free(indexPointer) } 117 | 118 | try paths.withGitStrArray { strArray in 119 | var strArray = strArray 120 | 121 | let flags = GIT_INDEX_ADD_DEFAULT.rawValue | GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH.rawValue 122 | 123 | // TODO: Implement options 124 | // Add the files to the index 125 | let status = git_index_add_all(indexPointer, &strArray, flags, nil, nil) 126 | 127 | guard status == GIT_OK.rawValue else { 128 | throw IndexError.failedToAddFile(errorMessage) 129 | } 130 | } 131 | // Write the index back to the repository 132 | try writeIndex(indexPointer: indexPointer) 133 | } 134 | 135 | /// Adds files to the index. 136 | /// 137 | /// - Parameter files: The file URLs. 138 | /// 139 | /// The files should be URLs to files in the repository. 140 | func add(files: [URL]) throws { 141 | // Get the relative paths of the files 142 | let paths = try files.map { try relativePath(for: $0) } 143 | 144 | // Add the files to the index 145 | try add(paths: paths) 146 | } 147 | 148 | /// Removes a file from the index. 149 | /// 150 | /// - Parameter path: The file path relative to the repository root directory. 151 | /// 152 | /// The path should be relative to the repository root directory. 153 | /// For example, `README.md` or `Sources/SwiftGitX/Repository.swift`. 154 | func remove(path: String) throws { 155 | // Read the index 156 | let indexPointer = try readIndexPointer() 157 | defer { git_index_free(indexPointer) } 158 | 159 | // Remove the file from the index 160 | let status = git_index_remove_bypath(indexPointer, path) 161 | 162 | guard status == GIT_OK.rawValue else { 163 | throw IndexError.failedToRemoveFile(errorMessage) 164 | } 165 | 166 | // Write the index back to the repository 167 | try writeIndex(indexPointer: indexPointer) 168 | } 169 | 170 | /// Removes a file from the index. 171 | /// 172 | /// - Parameter file: The file URL. 173 | /// 174 | /// The file should be a URL to a file in the repository. 175 | func remove(file: URL) throws { 176 | // Get the relative path of the file 177 | let relativePath = try relativePath(for: file) 178 | 179 | // Remove the file from the index 180 | try remove(path: relativePath) 181 | } 182 | 183 | /// Removes files from the index. 184 | /// 185 | /// - Parameter paths: The file paths relative to the repository root directory. 186 | /// 187 | /// The paths should be relative to the repository root directory. 188 | /// For example, `README.md` or `Sources/SwiftGitX/Repository.swift`. 189 | func remove(paths: [String]) throws { 190 | // Read the index 191 | let indexPointer = try readIndexPointer() 192 | defer { git_index_free(indexPointer) } 193 | 194 | // TODO: Implement options 195 | // Remove the files from the index 196 | try paths.withGitStrArray { strArray in 197 | var strArray = strArray 198 | let status = git_index_remove_all(indexPointer, &strArray, nil, nil) 199 | 200 | guard status == GIT_OK.rawValue else { 201 | throw IndexError.failedToRemoveFile(errorMessage) 202 | } 203 | } 204 | 205 | // Write the index back to the repository 206 | try writeIndex(indexPointer: indexPointer) 207 | } 208 | 209 | /// Removes files from the index. 210 | /// 211 | /// - Parameter files: The file URLs. 212 | /// 213 | /// The files should be URLs to files in the repository. 214 | func remove(files: [URL]) throws { 215 | // Get the relative paths of the files 216 | let paths = try files.map { try relativePath(for: $0) } 217 | 218 | // Remove the files from the index 219 | try remove(paths: paths) 220 | } 221 | 222 | /// Removes all files from the index. 223 | /// 224 | /// This method will clear the index. 225 | func removeAll() throws { 226 | // Read the index 227 | let indexPointer = try readIndexPointer() 228 | defer { git_index_free(indexPointer) } 229 | 230 | // Remove all files from the index 231 | let status = git_index_clear(indexPointer) 232 | 233 | guard status == GIT_OK.rawValue else { 234 | throw IndexError.failedToRemoveFile(errorMessage) 235 | } 236 | 237 | // Write the index back to the repository 238 | try writeIndex(indexPointer: indexPointer) 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Collections/ReferenceCollection.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | public enum ReferenceCollectionError: Error { 4 | case failedToList(String) 5 | } 6 | 7 | /// A collection of references and their operations. 8 | public struct ReferenceCollection: Sequence { 9 | private let repositoryPointer: OpaquePointer 10 | 11 | init(repositoryPointer: OpaquePointer) { 12 | self.repositoryPointer = repositoryPointer 13 | } 14 | 15 | // * I am not sure calling `git_error_last()` from a computed property is safe. 16 | // * Because libgit2 docs say that "The error message is thread-local. The git_error_last() call must happen on the 17 | // * same thread as the error in order to get the message." 18 | // * But, I think it is worth a try. 19 | private var errorMessage: String { 20 | String(cString: git_error_last().pointee.message) 21 | } 22 | 23 | /// Retrieve a reference by its full name. 24 | /// 25 | /// - Parameter fullName: The full name of the reference. 26 | /// (e.g. `refs/heads/main`, `refs/tags/v1.0.0`,`refs/remotes/origin/main`) 27 | /// 28 | /// - Returns: The reference with the specified name, or `nil` if it doesn't exist. 29 | public subscript(fullName: String) -> (any Reference)? { 30 | try? get(named: fullName) 31 | } 32 | 33 | /// Returns a reference by its full name. 34 | /// 35 | /// - Parameter fullName: The full name of the reference. 36 | /// (e.g. `refs/heads/main`, `refs/tags/v1.0.0`,`refs/remotes/origin/main`) 37 | /// 38 | /// - Returns: The reference with the specified name. 39 | /// 40 | /// - Throws: A `ReferenceError` if an error occurs. 41 | public func get(named fullName: String) throws -> (any Reference) { 42 | let referencePointer = try ReferenceFactory.lookupReferencePointer( 43 | fullName: fullName, 44 | repositoryPointer: repositoryPointer 45 | ) 46 | defer { git_reference_free(referencePointer) } 47 | 48 | return try ReferenceFactory.makeReference(pointer: referencePointer) 49 | } 50 | 51 | /// Returns a list of references. 52 | /// 53 | /// - Parameter glob: A glob pattern to filter the references (e.g. `refs/heads/*`, `refs/tags/*`). 54 | /// Default is `nil`. 55 | /// 56 | /// - Returns: A list of references. 57 | /// 58 | /// The reference can be a `Branch`, a `Tag`. 59 | /// 60 | /// - Throws: A `ReferenceCollectionError.failedToList` if an error occurs. 61 | public func list(glob: String? = nil) throws -> [any Reference] { 62 | var referenceIterator: UnsafeMutablePointer? 63 | defer { git_reference_iterator_free(referenceIterator) } 64 | 65 | let status = if let glob { 66 | git_reference_iterator_glob_new(&referenceIterator, repositoryPointer, glob) 67 | } else { 68 | git_reference_iterator_new(&referenceIterator, repositoryPointer) 69 | } 70 | 71 | guard status == GIT_OK.rawValue else { 72 | throw ReferenceCollectionError.failedToList(errorMessage) 73 | } 74 | 75 | var references = [any Reference]() 76 | while true { 77 | var referencePointer: OpaquePointer? 78 | defer { git_reference_free(referencePointer) } 79 | 80 | let nextStatus = git_reference_next(&referencePointer, referenceIterator) 81 | 82 | if nextStatus == GIT_ITEROVER.rawValue { 83 | break 84 | } else if nextStatus != GIT_OK.rawValue { 85 | throw ReferenceCollectionError.failedToList(errorMessage) 86 | } else if let referencePointer { 87 | let reference = try ReferenceFactory.makeReference(pointer: referencePointer) 88 | references.append(reference) 89 | } else { 90 | throw ReferenceCollectionError.failedToList("Failed to get reference") 91 | } 92 | } 93 | 94 | return references 95 | } 96 | 97 | public func makeIterator() -> ReferenceIterator { 98 | ReferenceIterator(repositoryPointer: repositoryPointer) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Collections/RemoteCollection.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import libgit2 3 | 4 | public enum RemoteCollectionError: Error, Equatable { 5 | case failedToList(String) 6 | case failedToAdd(String) 7 | case failedToRemove(String) 8 | case remoteAlreadyExists(String) 9 | } 10 | 11 | /// A collection of remotes and their operations. 12 | public struct RemoteCollection: Sequence { 13 | private let repositoryPointer: OpaquePointer 14 | 15 | init(repositoryPointer: OpaquePointer) { 16 | self.repositoryPointer = repositoryPointer 17 | } 18 | 19 | // * I am not sure calling `git_error_last()` from a computed property is safe. 20 | // * Because libgit2 docs say that "The error message is thread-local. The git_error_last() call must happen on the 21 | // * same thread as the error in order to get the message." 22 | // * But, I think it is worth a try. 23 | private var errorMessage: String { 24 | String(cString: git_error_last().pointee.message) 25 | } 26 | 27 | /// Retrieves a remote by its name. 28 | /// 29 | /// - Parameter name: The name of the remote. 30 | /// 31 | /// - Returns: The remote with the specified name, or `nil` if it doesn't exist. 32 | public subscript(name: String) -> Remote? { 33 | try? get(named: name) 34 | } 35 | 36 | /// Returns a remote by name. 37 | /// 38 | /// - Parameter name: The name of the remote. 39 | /// 40 | /// - Returns: The remote with the specified name. 41 | public func get(named name: String) throws -> Remote { 42 | let remotePointer = try ReferenceFactory.lookupRemotePointer(name: name, repositoryPointer: repositoryPointer) 43 | defer { git_remote_free(remotePointer) } 44 | 45 | return try Remote(pointer: remotePointer) 46 | } 47 | 48 | /// Returns a list of remotes. 49 | /// 50 | /// - Returns: An array of remotes. 51 | /// 52 | /// - Throws: `RemoteCollectionError.failedToList` if the remotes could not be listed. 53 | /// 54 | /// If you want to iterate over the remotes, you can use the `makeIterator()` method. 55 | /// Iterator continues to the next remote even if an error occurs while getting the remote. 56 | public func list() throws -> [Remote] { 57 | let remotes = try remoteNames.map { remoteName in 58 | try get(named: remoteName) 59 | } 60 | 61 | return remotes 62 | } 63 | 64 | /// Adds a new remote to the repository. 65 | /// 66 | /// - Parameters: 67 | /// - name: The name of the remote. 68 | /// - url: The URL of the remote. 69 | /// 70 | /// - Returns: The remote that was added. 71 | /// 72 | /// - Throws: `RemoteCollectionError.failedToAdd` if the remote could not be added. 73 | @discardableResult 74 | public func add(named name: String, at url: URL) throws -> Remote { 75 | var remotePointer: OpaquePointer? 76 | defer { git_remote_free(remotePointer) } 77 | 78 | // Create a new remote 79 | let status = git_remote_create(&remotePointer, repositoryPointer, name, url.absoluteString) 80 | 81 | guard let remotePointer, status == GIT_OK.rawValue else { 82 | switch status { 83 | case GIT_EEXISTS.rawValue: 84 | throw RemoteCollectionError.remoteAlreadyExists(errorMessage) 85 | default: 86 | throw RemoteCollectionError.failedToAdd(errorMessage) 87 | } 88 | } 89 | 90 | return try Remote(pointer: remotePointer) 91 | } 92 | 93 | /// Remove a remote from the repository. 94 | /// 95 | /// - Parameter remote: The remote to remove. 96 | /// 97 | /// - Throws: `RemoteCollectionError.failedToRemove` if the remote could not be removed. 98 | public func remove(_ remote: Remote) throws { 99 | let status = git_remote_delete(repositoryPointer, remote.name) 100 | 101 | guard status == GIT_OK.rawValue else { 102 | throw RemoteCollectionError.failedToRemove(errorMessage) 103 | } 104 | } 105 | 106 | public func makeIterator() -> RemoteIterator { 107 | RemoteIterator(remoteNames: (try? remoteNames) ?? [], repositoryPointer: repositoryPointer) 108 | } 109 | 110 | private var remoteNames: [String] { 111 | get throws { 112 | // Create a list to store the remote names 113 | var array = git_strarray() 114 | defer { git_strarray_free(&array) } 115 | 116 | // Get the remote names 117 | let status = git_remote_list(&array, repositoryPointer) 118 | 119 | guard status == GIT_OK.rawValue else { 120 | throw RemoteCollectionError.failedToList(errorMessage) 121 | } 122 | 123 | // Create a list to store the remote names 124 | var remoteNames = [String]() 125 | 126 | // Convert raw remote names to Swift strings 127 | for index in 0 ..< array.count { 128 | guard let rawRemoteName = array.strings.advanced(by: index).pointee 129 | else { 130 | throw RemoteCollectionError.failedToList("Failed to get remote name at index \(index)") 131 | } 132 | 133 | let remoteName = String(cString: rawRemoteName) 134 | remoteNames.append(remoteName) 135 | } 136 | 137 | return remoteNames 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Collections/StashCollection.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | public enum StashCollectionError: Error, Equatable { 4 | case noLocalChangesToSave 5 | case failedToSave(String) 6 | case failedToList(String) 7 | case failedToApply(String) 8 | case failedToDrop(String) 9 | case failedToPop(String) 10 | } 11 | 12 | /// A collection of stashes and their operations. 13 | public struct StashCollection: Sequence { 14 | private let repositoryPointer: OpaquePointer 15 | 16 | init(repositoryPointer: OpaquePointer) { 17 | self.repositoryPointer = repositoryPointer 18 | } 19 | 20 | private var errorMessage: String { 21 | String(cString: git_error_last().pointee.message) 22 | } 23 | 24 | /// Returns a list of stashes. 25 | /// 26 | /// - Returns: An array of stashes. 27 | /// 28 | /// - Throws: `StashCollectionError.failedToList` if the stashes could not be listed. 29 | public func list() throws -> [StashEntry] { 30 | // Define a context to store the stashes and the repository pointer 31 | class Context { 32 | var stashEntries: [StashEntry] 33 | var repositoryPointer: OpaquePointer 34 | 35 | init(stashEntries: [StashEntry], repositoryPointer: OpaquePointer) { 36 | self.stashEntries = stashEntries 37 | self.repositoryPointer = repositoryPointer 38 | } 39 | } 40 | 41 | // Define a callback to process each stash entry 42 | let callback: git_stash_cb = { index, message, oid, payload in 43 | guard let context = payload?.assumingMemoryBound(to: Context.self).pointee else { 44 | return -1 45 | } 46 | 47 | guard let oid = oid?.pointee, let message else { 48 | return -1 49 | } 50 | 51 | guard let target: Commit = try? ObjectFactory.lookupObject( 52 | oid: oid, 53 | repositoryPointer: context.repositoryPointer 54 | ) 55 | else { return -1 } 56 | 57 | let stashEntry = StashEntry( 58 | index: index, 59 | target: target, 60 | message: String(cString: message), 61 | stasher: target.author, 62 | date: target.date 63 | ) 64 | context.stashEntries.append(stashEntry) 65 | 66 | return 0 67 | } 68 | 69 | // List the stashes 70 | var context = Context(stashEntries: [], repositoryPointer: repositoryPointer) 71 | let status = withUnsafeMutablePointer(to: &context) { contextPointer in 72 | git_stash_foreach( 73 | repositoryPointer, 74 | callback, 75 | contextPointer 76 | ) 77 | } 78 | 79 | guard status == GIT_OK.rawValue else { 80 | throw StashCollectionError.failedToList("Failed to list stashes") 81 | } 82 | 83 | return context.stashEntries 84 | } 85 | 86 | /// Saves the local modifications to the stash. 87 | /// 88 | /// - Parameters: 89 | /// - message: The message associated with the stash. 90 | /// - options: The options to use when saving the stash. 91 | /// - stasher: The signature of the stasher. 92 | /// 93 | /// - Throws: `StashCollectionError.failedToSave` if the stash could not be saved, 94 | /// `StashCollectionError.noLocalChangesToSave` if there are no local changes to save, 95 | public func save( 96 | message: String? = nil, 97 | options: StashOption = .default, 98 | stasher: Signature? = nil 99 | ) throws { 100 | // Get the default signature if none is provided 101 | let stasher = try stasher ?? Signature.default(in: repositoryPointer) 102 | 103 | // Create a pointer to the stasher 104 | let stasherPointer = try ObjectFactory.makeSignaturePointer(signature: stasher) 105 | defer { git_signature_free(stasherPointer) } 106 | 107 | // Save the local modifications to the stash 108 | var oid = git_oid() 109 | let status = git_stash_save( 110 | &oid, 111 | repositoryPointer, 112 | stasherPointer, 113 | message, 114 | options.rawValue 115 | ) 116 | 117 | guard status == GIT_OK.rawValue else { 118 | switch status { 119 | case GIT_ENOTFOUND.rawValue: 120 | throw StashCollectionError.noLocalChangesToSave 121 | default: 122 | throw StashCollectionError.failedToSave(errorMessage) 123 | } 124 | } 125 | } 126 | 127 | // TODO: Implement apply options 128 | /// Applies the stash entry to the working directory. 129 | /// 130 | /// - Parameter stashEntry: The stash entry to apply. 131 | /// 132 | /// - Throws: `StashCollectionError.failedToApply` if the stash entry could not be applied. 133 | public func apply(_ stashEntry: StashEntry? = nil) throws { 134 | let stashIndex = stashEntry?.index ?? 0 135 | 136 | // Apply the stash entry 137 | // TODO: Handle GIT_EMERGECONFLICT 138 | let status = git_stash_apply(repositoryPointer, stashIndex, nil) 139 | 140 | guard status == GIT_OK.rawValue else { 141 | throw StashCollectionError.failedToApply(errorMessage) 142 | } 143 | } 144 | 145 | // TODO: Implement apply options 146 | /// Applies the stash entry to the working directory and removes it from the stash list. 147 | /// 148 | /// - Parameter stashEntry: The stash entry to pop. 149 | /// 150 | /// - Throws: `StashCollectionError.failedToPop` if the stash entry could not be popped. 151 | public func pop(_ stashEntry: StashEntry? = nil) throws { 152 | let stashIndex = stashEntry?.index ?? 0 153 | 154 | // Pop the stash entry 155 | // TODO: Handle GIT_EMERGECONFLICT 156 | let status = git_stash_pop(repositoryPointer, stashIndex, nil) 157 | 158 | guard status == GIT_OK.rawValue else { 159 | throw StashCollectionError.failedToPop(errorMessage) 160 | } 161 | } 162 | 163 | /// Removes the stash entry from the stash list. 164 | /// 165 | /// - Parameter stashEntry: The stash entry to drop. 166 | /// 167 | /// - Throws: `StashCollectionError.failedToDrop` if the stash entry could not be dropped. 168 | public func drop(_ stashEntry: StashEntry? = nil) throws { 169 | let stashIndex = stashEntry?.index ?? 0 170 | 171 | // Drop the stash entry 172 | let status = git_stash_drop(repositoryPointer, stashIndex) 173 | 174 | guard status == GIT_OK.rawValue else { 175 | throw StashCollectionError.failedToDrop(errorMessage) 176 | } 177 | } 178 | 179 | // TODO: Create a true iterator 180 | public func makeIterator() -> StashIterator { 181 | StashIterator(entries: (try? list()) ?? []) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Collections/TagCollection.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | public enum TagCollectionError: Error { 4 | case failedToList(String) 5 | case failedToCreate(String) 6 | } 7 | 8 | /// A collection of tags and their operations. 9 | public struct TagCollection: Sequence { 10 | private var repositoryPointer: OpaquePointer 11 | 12 | init(repositoryPointer: OpaquePointer) { 13 | self.repositoryPointer = repositoryPointer 14 | } 15 | 16 | // * I am not sure calling `git_error_last()` from a computed property is safe. 17 | // * Because libgit2 docs say that "The error message is thread-local. The git_error_last() call must happen on the 18 | // * same thread as the error in order to get the message." 19 | // * But, I think it is worth a try. 20 | private var errorMessage: String { 21 | String(cString: git_error_last().pointee.message) 22 | } 23 | 24 | public subscript(name: String) -> Tag? { 25 | try? get(named: name) 26 | } 27 | 28 | public func get(named name: String) throws -> Tag { 29 | try ObjectFactory.lookupTag(name: name, repositoryPointer: repositoryPointer) 30 | } 31 | 32 | // TODO: Maybe we can write it as TagIterator 33 | public func list() throws -> [Tag] { 34 | var array = git_strarray() 35 | defer { git_strarray_free(&array) } 36 | 37 | let status = git_tag_list(&array, repositoryPointer) 38 | 39 | guard status == GIT_OK.rawValue else { 40 | throw TagCollectionError.failedToList(errorMessage) 41 | } 42 | 43 | var tags = [Tag]() 44 | for index in 0 ..< array.count { 45 | let tagName = String(cString: array.strings.advanced(by: index).pointee!) 46 | 47 | let tag = try get(named: tagName) 48 | tags.append(tag) 49 | } 50 | 51 | return tags 52 | } 53 | 54 | /** 55 | Creates a new tag. 56 | 57 | - Parameters: 58 | - name: The name of the tag. 59 | - target: The target object for the tag. 60 | - type: The type of the tag. Default is `.annotated`. 61 | - tagger: The signature of the tagger. If not provided, the default signature in the repository will be used. 62 | - message: The message associated with the tag. If not provided, an empty string will be used. 63 | - force: If `true`, the tag will be overwritten if it already exists. Default is `false`. 64 | 65 | - Returns: The created `Tag` object. 66 | 67 | - Throws: `TagCollectionError.failedToCreate` if the tag could not be created. 68 | 69 | - Note: If the tag already exists and `force` is `false`, an error will be thrown. 70 | */ 71 | @discardableResult 72 | public func create( 73 | named name: String, 74 | target: any Object, 75 | type: TagType = .annotated, 76 | tagger: Signature? = nil, 77 | message: String? = nil, 78 | force: Bool = false 79 | ) throws -> Tag { 80 | let targetPointer = try ObjectFactory.lookupObjectPointer( 81 | oid: target.id.raw, 82 | type: GIT_OBJECT_ANY, 83 | repositoryPointer: repositoryPointer 84 | ) 85 | defer { git_object_free(targetPointer) } 86 | 87 | var tagID = git_oid() 88 | 89 | switch type { 90 | case .annotated: 91 | // Get the default signature if none is provided 92 | let tagger = try tagger ?? Signature.default(in: repositoryPointer) 93 | 94 | // Create a pointer to the tagger 95 | let taggerPointer = try ObjectFactory.makeSignaturePointer(signature: tagger) 96 | defer { git_signature_free(taggerPointer) } 97 | 98 | // Create an annotated tag 99 | let status = git_tag_create( 100 | &tagID, 101 | repositoryPointer, 102 | name, 103 | targetPointer, 104 | taggerPointer, 105 | message ?? "", 106 | force ? 1 : 0 107 | ) 108 | 109 | guard status == GIT_OK.rawValue else { 110 | throw TagCollectionError.failedToCreate(errorMessage) 111 | } 112 | 113 | case .lightweight: 114 | // Create a lightweight tag 115 | let status = git_tag_create_lightweight( 116 | &tagID, 117 | repositoryPointer, 118 | name, 119 | targetPointer, 120 | force ? 1 : 0 121 | ) 122 | 123 | guard status == GIT_OK.rawValue else { 124 | throw TagCollectionError.failedToCreate(errorMessage) 125 | } 126 | } 127 | 128 | // Lookup the tag by its name 129 | return try get(named: name) 130 | } 131 | 132 | public func makeIterator() -> TagIterator { 133 | TagIterator(repositoryPointer: repositoryPointer) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Helpers/Constants.swift: -------------------------------------------------------------------------------- 1 | enum GitDirectoryConstants { 2 | /// The directory of the heads (branches). 3 | static let heads = "refs/heads/" 4 | 5 | /// The directory of the remotes. 6 | static let remotes = "refs/remotes/" 7 | 8 | /// The directory of the tags. 9 | static let tags = "refs/tags/" 10 | } 11 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Helpers/Extensions/Array+withGitStrArray.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import libgit2 3 | 4 | extension [String] { 5 | /// Converts the array of strings to a `git_strarray` instance. 6 | /// 7 | /// - Returns: A `git_strarray` instance. 8 | /// 9 | /// - Important: The returned `git_strarray` instance must be freed using `git_strarray_free`. 10 | var gitStrArray: git_strarray { 11 | // Create an array of C strings 12 | var cStrings = self.map { strdup($0) } 13 | 14 | // Create a pointer to the C strings 15 | let pointer = UnsafeMutablePointer?>.allocate(capacity: cStrings.count) 16 | pointer.initialize(from: &cStrings, count: cStrings.count) 17 | 18 | // Create a git_strarray instance 19 | return git_strarray(strings: pointer, count: cStrings.count) 20 | } 21 | 22 | /// Executes a closure with a `git_strarray` instance created from the array of strings. 23 | /// 24 | /// - Parameter body: The closure to execute. 25 | /// 26 | /// - Returns: The result of the closure. 27 | /// 28 | /// - Throws: Any error thrown by the closure. 29 | /// 30 | /// - Note: The `git_strarray` instance is freed after the closure is executed. You shouldn't use it outside the 31 | /// closure. 32 | func withGitStrArray(_ body: (git_strarray) throws -> T) rethrows -> T { 33 | var strArray = gitStrArray 34 | defer { git_strarray_free(&strArray) } 35 | 36 | return try body(strArray) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Helpers/Extensions/URL+relativePath.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension URL { 4 | /// An error thrown when the URL is not a descendant of the base URL. 5 | enum RelativePathError: Error { 6 | /// The URL is not a descendant of the base URL. 7 | case notDescendantOfBase 8 | } 9 | 10 | /// Returns the relative path of the URL. 11 | /// 12 | /// - Parameter base: The base URL to get the relative path from. 13 | /// 14 | /// - Returns: The relative path of the URL. 15 | /// 16 | /// - Throws: An error if the URL is not a descendant of the base URL. 17 | func relativePath(from base: URL) throws -> String { 18 | guard path.hasPrefix(base.path) else { 19 | throw RelativePathError.notDescendantOfBase 20 | } 21 | 22 | var relativePath = String(path.dropFirst(base.path.count)) 23 | if relativePath.hasPrefix("/") { 24 | relativePath = String(relativePath.dropFirst()) 25 | } 26 | 27 | return relativePath 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Helpers/Factory/ObjectFactory.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | /// An object factory that creates objects from pointers or object ids. 4 | /// 5 | /// The factory creates objects of type `Commit`, `Tree`, `Blob`, or `Tag`. 6 | /// - Note: This is an internal API. Must be used for the specified object types. 7 | enum ObjectFactory { 8 | // ? Should we pass `type` parameter to restrict the object type? 9 | /// Lookups an object of the specified type using the given object ID and repository pointer. 10 | /// 11 | /// - Parameters: 12 | /// - oid: The raw object ID of the desired object. 13 | /// - repositoryPointer: The pointer to the repository. 14 | /// 15 | /// - Returns: A object of the specified type. 16 | static func lookupObject(oid: git_oid, repositoryPointer: OpaquePointer) throws -> ObjectType { 17 | let object = try lookupObject(oid: oid, repositoryPointer: repositoryPointer) 18 | 19 | guard let object = object as? ObjectType else { 20 | throw ObjectError.invalid("Specified object type is invalid") 21 | } 22 | 23 | return object 24 | } 25 | 26 | /// Lookups an object of any type from the given object ID in the specified repository. 27 | /// 28 | /// - Parameters: 29 | /// - oid: The raw object ID of the desired object. 30 | /// - repositoryPointer: The pointer to the repository. 31 | /// 32 | /// - Returns: A object of type `Commit`, `Tree`, `Blob`, or `Tag` based on the type of the object. 33 | static func lookupObject(oid: git_oid, repositoryPointer: OpaquePointer) throws -> any Object { 34 | let pointer = try lookupObjectPointer(oid: oid, type: GIT_OBJECT_ANY, repositoryPointer: repositoryPointer) 35 | defer { git_object_free(pointer) } 36 | 37 | return try makeObject(pointer: pointer) 38 | } 39 | 40 | static func lookupTag(name: String, repositoryPointer: OpaquePointer) throws -> Tag { 41 | // Add the prefix to the tag name 42 | let fullName = GitDirectoryConstants.tags + name 43 | 44 | // Lookup the tag by its name 45 | let objectPointer = try ObjectFactory.lookupObjectPointer( 46 | revision: fullName, 47 | repositoryPointer: repositoryPointer 48 | ) 49 | defer { git_object_free(objectPointer) } 50 | 51 | // Create a tag object based on the object pointer 52 | switch git_object_type(objectPointer) { 53 | // If the tag is annotated, its type will be `GIT_OBJECT_TAG`. 54 | case GIT_OBJECT_TAG: 55 | let tag = try Tag(pointer: objectPointer) 56 | 57 | // If the tag name is the same as the requested name, return the tag. 58 | // If the tag name is different, it means it is a lightweight tag pointing to a tag object. 59 | return tag.name == name ? tag : Tag(name: name, target: tag) 60 | 61 | // If the tag is lightweight, its id will be the same as the target object. 62 | // So, its type will be the type of the target object. 63 | default: 64 | let target = try ObjectFactory.makeObject(pointer: objectPointer) 65 | return Tag(name: name, target: target) 66 | } 67 | } 68 | 69 | /// Creates an object based on the given object pointer. 70 | /// 71 | /// - Parameters: 72 | /// - pointer: The opaque pointer representing the object. 73 | /// 74 | /// - Returns: A object of type `Commit`, `Tree`, `Blob`, or `Tag` based on the type of the object. 75 | private static func makeObject(pointer: OpaquePointer) throws -> any Object { 76 | let type = git_object_type(pointer) 77 | 78 | return switch type { 79 | case GIT_OBJECT_COMMIT: 80 | try Commit(pointer: pointer) 81 | case GIT_OBJECT_TREE: 82 | try Tree(pointer: pointer) 83 | case GIT_OBJECT_BLOB: 84 | try Blob(pointer: pointer) 85 | case GIT_OBJECT_TAG: 86 | try Tag(pointer: pointer) 87 | default: 88 | throw ObjectError.invalid("Invalid object type") 89 | } 90 | } 91 | 92 | /// Creates an object pointer from the given object ID and type in the specified repository. 93 | /// 94 | /// - Parameters: 95 | /// - oid: The raw object id. 96 | /// - type: The raw type of the object. 97 | /// - repositoryPointer: The pointer to the repository. 98 | /// 99 | /// - Returns: A pointer to the object. 100 | /// 101 | /// - Throws: `ObjectError.invalid` if the object is not found or an error occurs. 102 | /// 103 | /// - Important: The returned object pointer must be released with `git_object_free` when no longer needed. 104 | static func lookupObjectPointer( 105 | oid: git_oid, 106 | type: git_object_t, 107 | repositoryPointer: OpaquePointer 108 | ) throws -> OpaquePointer { 109 | var pointer: OpaquePointer? 110 | 111 | var oid = oid 112 | let status = git_object_lookup(&pointer, repositoryPointer, &oid, type) 113 | 114 | guard let pointer, status == GIT_OK.rawValue else { 115 | let errorMessage = String(cString: git_error_last().pointee.message) 116 | throw ObjectError.invalid(errorMessage) 117 | } 118 | 119 | return pointer 120 | } 121 | 122 | static func lookupObject(revision: String, repositoryPointer: OpaquePointer) throws -> any Object { 123 | let objectPointer = try lookupObjectPointer(revision: revision, repositoryPointer: repositoryPointer) 124 | defer { git_object_free(objectPointer) } 125 | 126 | return try makeObject(pointer: objectPointer) 127 | } 128 | 129 | static func lookupObjectPointer(revision: String, repositoryPointer: OpaquePointer) throws -> OpaquePointer { 130 | var objectPointer: OpaquePointer? 131 | 132 | // Lookup the object by its revision 133 | let status = git_revparse_single(&objectPointer, repositoryPointer, revision) 134 | 135 | guard let objectPointer, status == GIT_OK.rawValue else { 136 | let errorMessage = String(cString: git_error_last().pointee.message) 137 | throw ObjectError.invalid(errorMessage) 138 | } 139 | 140 | return objectPointer 141 | } 142 | 143 | /// Peels an object to the specified type. 144 | /// 145 | /// - Parameters: 146 | /// - oid: The object id of the object to peel. 147 | /// - type: The target type of the object. 148 | /// 149 | /// - Returns: A pointer to the peeled object. 150 | /// 151 | /// - Important: The returned object pointer must be released with `git_object_free` when no longer needed. 152 | static func peelObjectPointer( 153 | oid: git_oid, 154 | targetType: git_object_t, 155 | repositoryPointer: OpaquePointer 156 | ) throws -> OpaquePointer { 157 | // Lookup the object by its object id 158 | let objectPointer = try ObjectFactory.lookupObjectPointer( 159 | oid: oid, 160 | type: GIT_OBJECT_ANY, 161 | repositoryPointer: repositoryPointer 162 | ) 163 | defer { git_object_free(objectPointer) } 164 | 165 | // Create a pointer to the peeled object 166 | var peeledPointer: OpaquePointer? 167 | 168 | // Peel the object to the specified type 169 | let status = git_object_peel(&peeledPointer, objectPointer, targetType) 170 | 171 | guard let peeledPointer, status == GIT_OK.rawValue else { 172 | let errorMessage = String(cString: git_error_last().pointee.message) 173 | throw ObjectError.invalid(errorMessage) 174 | } 175 | 176 | return peeledPointer 177 | } 178 | 179 | /// Creates a signature pointer from the given signature. 180 | /// 181 | /// - Parameter signature: The signature to create a pointer from. 182 | /// 183 | /// - Returns: A pointer to the signature. 184 | /// 185 | /// - Throws: `SignatureError.invalid` if the signature is invalid or an error occurs. 186 | /// 187 | /// - Important: The returned signature pointer must be released with `git_signature_free` when no longer needed. 188 | static func makeSignaturePointer(signature: Signature) throws -> UnsafeMutablePointer { 189 | var signaturePointer: UnsafeMutablePointer? 190 | 191 | let status = git_signature_new( 192 | &signaturePointer, 193 | signature.name, 194 | signature.email, 195 | git_time_t(signature.date.timeIntervalSince1970), 196 | Int32(signature.timezone.secondsFromGMT() / 60) 197 | ) 198 | 199 | guard let signaturePointer, status == GIT_OK.rawValue else { 200 | let errorMessage = String(cString: git_error_last().pointee.message) 201 | throw SignatureError.invalid(errorMessage) 202 | } 203 | 204 | return signaturePointer 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Helpers/Factory/ReferenceFactory.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | enum ReferenceFactory { 4 | /// Creates a reference based on the given pointer. 5 | /// 6 | /// - Parameter pointer: The reference pointer. 7 | /// 8 | /// - Returns: A reference of type `Branch` or `Tag` based on the type of the reference. 9 | static func makeReference(pointer: OpaquePointer) throws -> any Reference { 10 | if git_reference_is_branch(pointer) == 1 || git_reference_is_remote(pointer) == 1 { 11 | return try Branch(pointer: pointer) 12 | } else if git_reference_is_tag(pointer) == 1 { 13 | // Tag is a object as well as a reference. 14 | // But we need to get `git_tag` object to get the tag properties. 15 | // To get the `git_tag` object, we need to lookup the tag by its name. 16 | 17 | // Get the tag name and the repository pointer 18 | let rawName = git_reference_shorthand(pointer) 19 | let repositoryPointer = git_reference_owner(pointer) 20 | 21 | guard let rawName, let repositoryPointer else { 22 | throw ReferenceError.invalid("Invalid reference") 23 | } 24 | 25 | // Lookup the tag by its full name 26 | let tag = try ObjectFactory.lookupTag( 27 | name: String(cString: rawName), 28 | repositoryPointer: repositoryPointer 29 | ) 30 | 31 | return tag 32 | } else { 33 | throw ReferenceError.invalid("Invalid reference type") 34 | } 35 | } 36 | 37 | /// Looks up a reference pointer in a given repository. 38 | /// 39 | /// - Parameters: 40 | /// - fullName: The full name of the reference. 41 | /// - repositoryPointer: The opaque pointer to the repository. 42 | /// 43 | /// - Returns: The opaque pointer to the reference. 44 | /// 45 | /// - Important: The returned reference pointer must be released with `git_reference_free` when no longer needed. 46 | static func lookupReferencePointer(fullName: String, repositoryPointer: OpaquePointer) throws -> OpaquePointer { 47 | var pointer: OpaquePointer? 48 | 49 | let status = git_reference_lookup(&pointer, repositoryPointer, fullName) 50 | 51 | guard status == GIT_OK.rawValue, let referencePointer = pointer else { 52 | switch status { 53 | case GIT_ENOTFOUND.rawValue: 54 | throw ReferenceError.notFound 55 | default: 56 | let errorMessage = String(cString: git_error_last().pointee.message) 57 | throw ReferenceError.invalid(errorMessage) 58 | } 59 | } 60 | 61 | return referencePointer 62 | } 63 | 64 | static func lookupBranchPointer( 65 | name: String, 66 | type: git_branch_t, 67 | repositoryPointer: OpaquePointer 68 | ) throws -> OpaquePointer { 69 | var pointer: OpaquePointer? 70 | 71 | let status = git_branch_lookup(&pointer, repositoryPointer, name, type) 72 | 73 | guard status == GIT_OK.rawValue, let branchPointer = pointer else { 74 | switch status { 75 | case GIT_ENOTFOUND.rawValue: 76 | throw ReferenceError.notFound 77 | default: 78 | let errorMessage = String(cString: git_error_last().pointee.message) 79 | throw ReferenceError.invalid(errorMessage) 80 | } 81 | } 82 | 83 | return branchPointer 84 | } 85 | 86 | static func lookupRemotePointer(name: String, repositoryPointer: OpaquePointer) throws -> OpaquePointer { 87 | var pointer: OpaquePointer? 88 | 89 | let status = git_remote_lookup(&pointer, repositoryPointer, name) 90 | 91 | guard status == GIT_OK.rawValue, let remotePointer = pointer else { 92 | let errorMessage = String(cString: git_error_last().pointee.message) 93 | 94 | switch status { 95 | case GIT_ENOTFOUND.rawValue: 96 | throw RemoteError.notFound(errorMessage) 97 | default: 98 | throw RemoteError.invalid(errorMessage) 99 | } 100 | } 101 | 102 | return remotePointer 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Helpers/LibGit2RawRepresentable.swift: -------------------------------------------------------------------------------- 1 | /// An internal protocol for types that can be represented by a raw libgit2 struct. 2 | protocol LibGit2RawRepresentable: Equatable, Hashable { 3 | associatedtype RawType 4 | 5 | /// Initializes the type with a raw libgit2 struct. 6 | /// 7 | /// - Parameter raw: The raw libgit2 struct. 8 | init(raw: RawType) 9 | } 10 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Models/Diff/Diff.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | /// Represents the differences between two trees. 4 | public struct Diff: Equatable, Hashable { 5 | /// The changed files in the diff. 6 | /// 7 | /// Each delta represents a file change. The delta contains the status of the change, 8 | /// the old and new file, and the flags. 9 | /// 10 | /// If you want to get each file change in detail, you can use the ``patches`` property. 11 | /// It provides changes line by line. Each patch represents a file change. 12 | public let changes: [Delta] 13 | 14 | /// The file patches in the diff. Each patch represents a file change. 15 | /// 16 | /// A patch is a collection of hunks that represent the changes in a file. 17 | /// Each hunk is a collection of lines that represent the changes in a specific part of the file. 18 | public let patches: [Patch] 19 | 20 | init(pointer: OpaquePointer) { 21 | var deltas = [Delta]() 22 | var patches = [Patch]() 23 | 24 | // Get the number of deltas in the diff 25 | let numberOfDeltas = git_diff_num_deltas(pointer) 26 | 27 | // Iterate over the deltas and append them to the array 28 | for index in 0 ..< numberOfDeltas { 29 | let deltaPointer = git_diff_get_delta(pointer, index) 30 | 31 | // If delta pointer is not nil, create a new delta 32 | if let rawDelta = deltaPointer?.pointee { 33 | // Create a new delta from the raw delta 34 | let delta = Delta(raw: rawDelta) 35 | // Append the delta to the array 36 | deltas.append(delta) 37 | } 38 | 39 | // Create patches 40 | var patchPointer: OpaquePointer? 41 | defer { git_patch_free(patchPointer) } 42 | 43 | let patchStatus = git_patch_from_diff(&patchPointer, pointer, index) 44 | 45 | // If the patch pointer is not nil, create a new patch 46 | if let patchPointer, patchStatus == GIT_OK.rawValue { 47 | let patch = Patch(pointer: patchPointer) 48 | patches.append(patch) 49 | } 50 | } 51 | 52 | changes = deltas 53 | self.patches = patches 54 | } 55 | } 56 | 57 | // MARK: - Structs 58 | 59 | public extension Diff { 60 | struct Delta: LibGit2RawRepresentable { 61 | /// The type of the delta. 62 | public let type: DeltaType 63 | 64 | /// The `oldFile` represents the "from" side of the diff. 65 | public let oldFile: File 66 | 67 | /// The `newFile` represents the "to" side of the diff. 68 | public let newFile: File 69 | 70 | /// The flags of the delta. 71 | public let flags: [Flag] 72 | 73 | /// The similarity between the files for "renamed" or "copied" status (0-100). 74 | public let similarity: Int 75 | 76 | /// The number of files in the delta. 77 | public let numberOfFiles: Int 78 | 79 | // Represents git_diff_file in libgit2. 80 | let raw: git_diff_delta 81 | 82 | init(raw: git_diff_delta) { 83 | type = DeltaType(rawValue: Int(raw.status.rawValue))! 84 | 85 | oldFile = File(raw: raw.old_file) 86 | newFile = File(raw: raw.new_file) 87 | 88 | flags = Flag.from(raw.flags) 89 | similarity = Int(raw.similarity) 90 | numberOfFiles = Int(raw.nfiles) 91 | 92 | self.raw = raw 93 | } 94 | 95 | public static func == (lhs: Diff.Delta, rhs: Diff.Delta) -> Bool { 96 | lhs.type == rhs.type && 97 | lhs.oldFile == rhs.oldFile && 98 | lhs.newFile == rhs.newFile && 99 | lhs.flags == rhs.flags && 100 | lhs.similarity == rhs.similarity && 101 | lhs.numberOfFiles == rhs.numberOfFiles 102 | } 103 | 104 | public func hash(into hasher: inout Hasher) { 105 | hasher.combine(type) 106 | hasher.combine(oldFile) 107 | hasher.combine(newFile) 108 | hasher.combine(flags) 109 | hasher.combine(similarity) 110 | hasher.combine(numberOfFiles) 111 | } 112 | } 113 | 114 | struct File: LibGit2RawRepresentable { 115 | /// The ID of the object. 116 | public let id: OID 117 | 118 | /// The path of the file relative to the repository working directory. 119 | public let path: String 120 | 121 | /// The size of the entry in bytes. 122 | public let size: Int 123 | 124 | /// The flags of the file. 125 | public let flags: [Flag] 126 | 127 | /// The mode of the file. 128 | public let mode: FileMode 129 | 130 | let raw: git_diff_file 131 | 132 | init(raw: git_diff_file) { 133 | id = OID(raw: raw.id) 134 | path = String(cString: raw.path) 135 | size = Int(raw.size) 136 | flags = Flag.from(raw.flags) 137 | mode = FileMode(raw: git_filemode_t(rawValue: UInt32(raw.mode))) 138 | 139 | self.raw = raw 140 | } 141 | 142 | public static func == (lhs: Diff.File, rhs: Diff.File) -> Bool { 143 | lhs.id == rhs.id && 144 | lhs.path == rhs.path && 145 | lhs.size == rhs.size && 146 | lhs.flags == rhs.flags && 147 | lhs.mode == rhs.mode 148 | } 149 | 150 | public func hash(into hasher: inout Hasher) { 151 | hasher.combine(id) 152 | hasher.combine(path) 153 | hasher.combine(size) 154 | hasher.combine(flags) 155 | hasher.combine(mode) 156 | } 157 | } 158 | } 159 | 160 | // MARK: - Enums 161 | 162 | public extension Diff { 163 | // Represents git_diff_flag_t enum in libgit2 164 | enum Flag { 165 | /// The file is binary. 166 | case binary 167 | 168 | /// The file is not binary. 169 | case notBinary 170 | 171 | /// The file id is valid. 172 | case validID 173 | 174 | /// The file exists at this side of the delta. 175 | case exists 176 | 177 | /// The file size is valid. 178 | case validSize 179 | 180 | static func from(_ flags: UInt32) -> [Flag] { 181 | var result = [Flag]() 182 | 183 | if flags & GIT_DIFF_FLAG_BINARY.rawValue != 0 { 184 | result.append(.binary) 185 | } 186 | 187 | if flags & GIT_DIFF_FLAG_NOT_BINARY.rawValue != 0 { 188 | result.append(.notBinary) 189 | } 190 | 191 | if flags & GIT_DIFF_FLAG_VALID_ID.rawValue != 0 { 192 | result.append(.validID) 193 | } 194 | 195 | if flags & GIT_DIFF_FLAG_EXISTS.rawValue != 0 { 196 | result.append(.exists) 197 | } 198 | 199 | if flags & GIT_DIFF_FLAG_VALID_SIZE.rawValue != 0 { 200 | result.append(.validSize) 201 | } 202 | 203 | return result 204 | } 205 | } 206 | } 207 | 208 | public extension Diff { 209 | // Represents git_delta_t enum in libgit2 210 | /// Represents what type of change is described by ``Delta``. 211 | enum DeltaType: Int { 212 | /// No changes 213 | case unmodified = 0 214 | 215 | /// Entry does not exist in old version 216 | case added 217 | 218 | /// Entry does not exist in new version 219 | case deleted 220 | 221 | /// Entry content changed between old and new 222 | case modified 223 | 224 | /// Entry was renamed between old and new 225 | case renamed 226 | 227 | /// Entry was copied from another old entry 228 | case copied 229 | 230 | /// Entry is ignored item in working tree 231 | case ignored 232 | 233 | /// Entry is untracked item in working tree 234 | case untracked 235 | 236 | /// Type of entry changed between old and new 237 | case typeChange 238 | 239 | /// Entry is unreadable 240 | case unreadable 241 | 242 | /// Entry in the index is conflicted 243 | case conflicted 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Models/Diff/Patch.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | // ? Can we improve the implementation of the Patch struct? 4 | /// A patch represents changes to a single file. 5 | /// 6 | /// It contains a sequence of hunks, each of which represents a contiguous section of the file. 7 | /// Each hunk contains a header and a sequence of lines. Each line represents a change in the file. 8 | public struct Patch: Equatable, Hashable { 9 | /// The delta associated with the patch. 10 | public let delta: Diff.Delta 11 | 12 | /// The hunks in the patch. 13 | public let hunks: [Hunk] 14 | 15 | init(pointer: OpaquePointer) { 16 | // Get the delta associated with the patch 17 | let deltaPointer = git_patch_get_delta(pointer) 18 | delta = Diff.Delta(raw: deltaPointer!.pointee) 19 | 20 | // Get hunks 21 | var hunks = [Hunk]() 22 | 23 | let numberOfHunks = git_patch_num_hunks(pointer) 24 | 25 | for index in 0 ..< numberOfHunks { 26 | var hunkPointer: UnsafePointer? 27 | var linesCountInHunk = 0 28 | 29 | let hunkStatus = git_patch_get_hunk(&hunkPointer, &linesCountInHunk, pointer, index) 30 | 31 | guard let rawHunk = hunkPointer?.pointee, hunkStatus == GIT_OK.rawValue else { 32 | continue 33 | } 34 | 35 | // Get lines 36 | var lines = [Hunk.Line]() 37 | 38 | for lineIndex in 0 ..< linesCountInHunk { 39 | var linePointer: UnsafePointer? 40 | let lineStatus = git_patch_get_line_in_hunk(&linePointer, pointer, index, lineIndex) 41 | 42 | guard let rawLine = linePointer?.pointee, lineStatus == GIT_OK.rawValue else { 43 | continue 44 | } 45 | 46 | let line = Hunk.Line(raw: rawLine) 47 | lines.append(line) 48 | } 49 | 50 | let hunk = Hunk(raw: rawHunk, lines: lines) 51 | hunks.append(hunk) 52 | } 53 | 54 | self.hunks = hunks 55 | } 56 | 57 | /// A hunk represents a contiguous section of a file. 58 | /// 59 | /// It contains a header and a sequence of lines. 60 | /// Each line represents a change in the file. 61 | public struct Hunk: Equatable, Hashable { 62 | /// The header of the hunk. 63 | public let header: String 64 | 65 | /// The lines in the hunk. Each line represents a change in the file. 66 | public let lines: [Line] 67 | 68 | /// The starting line number in the old file. 69 | public let oldStart: Int 70 | 71 | /// The number of lines in the old file. 72 | public let oldLines: Int 73 | 74 | /// The starting line number in the new file. 75 | public let newStart: Int 76 | 77 | /// The number of lines in the new file. 78 | public let newLines: Int 79 | 80 | init(raw: git_diff_hunk, lines: [Line]) { 81 | var header = raw.header 82 | self.header = withUnsafePointer(to: &header) { 83 | $0.withMemoryRebound(to: UInt8.self, capacity: raw.header_len) { 84 | String(cString: $0) 85 | } 86 | } 87 | 88 | oldStart = Int(raw.old_start) 89 | oldLines = Int(raw.old_lines) 90 | 91 | newStart = Int(raw.new_start) 92 | newLines = Int(raw.new_lines) 93 | 94 | self.lines = lines 95 | } 96 | 97 | // swiftlint:disable nesting 98 | 99 | /// A line represents a change in a file. 100 | public struct Line: Equatable, Hashable { 101 | /// The type of the line in the hunk. 102 | /// 103 | /// - SeeAlso: For details ``SwiftGitX/Patch/Hunk/LineType`` 104 | public let type: LineType 105 | 106 | /// The content of the line. 107 | public let content: String 108 | 109 | /// The offset of the content in the line. 110 | public let contentOffset: Int 111 | 112 | /// The line number in the file. 113 | public let lineNumber: Int 114 | 115 | /// The number of new line character (\\n) in the line. 116 | public let numberOfNewLines: Int 117 | 118 | init(raw: git_diff_line) { 119 | type = LineType(raw: git_diff_line_t(UInt32(raw.origin))) 120 | 121 | let buffer = UnsafeRawBufferPointer(start: raw.content, count: raw.content_len) 122 | content = String(decoding: buffer, as: UTF8.self) 123 | 124 | contentOffset = Int(raw.content_offset) 125 | 126 | // Old file line number and new file line number are two distinct variables in libgit 127 | // but generally one of it is -1. We can use the non -1 value as the line number. 128 | // ? Is this a good approach? Should we separate the line number into two variables? 129 | lineNumber = raw.old_lineno == -1 ? Int(raw.new_lineno) : Int(raw.old_lineno) 130 | 131 | numberOfNewLines = Int(raw.num_lines) 132 | } 133 | } 134 | 135 | /// The type of the line in the hunk. 136 | /// 137 | /// The type of the line can be a context line, an addition line, or a deletion line. 138 | /// 139 | /// If the line has no newline character at the end, 140 | /// it can be a context EOF, an addition EOF, or a deletion EOF. 141 | public enum LineType: String { 142 | case context = " " 143 | case addition = "+" 144 | case deletion = "-" 145 | 146 | case contextEOF = "=" 147 | case additionEOF = ">" 148 | case deletionEOF = "<" 149 | 150 | init(raw: git_diff_line_t) { 151 | self = switch raw { 152 | case GIT_DIFF_LINE_CONTEXT: 153 | .context 154 | case GIT_DIFF_LINE_ADDITION: 155 | .addition 156 | case GIT_DIFF_LINE_DELETION: 157 | .deletion 158 | case GIT_DIFF_LINE_CONTEXT_EOFNL: 159 | .contextEOF 160 | case GIT_DIFF_LINE_ADD_EOFNL: 161 | .additionEOF 162 | case GIT_DIFF_LINE_DEL_EOFNL: 163 | .deletionEOF 164 | default: 165 | .context 166 | } 167 | } 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Models/Diff/StatusEntry.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | /// Represents the status of a file in the repository. 4 | public struct StatusEntry: LibGit2RawRepresentable { 5 | /// The status of the file. 6 | /// 7 | /// This is an array of ``Status-swift.enum`` cases because a file can have multiple statuses. 8 | /// For example, if a file is modified than staged, it will be ``Status-swift.enum/indexModified`` but if it is 9 | /// modified again before the commit, it will be ``Status-swift.enum/workingTreeModified`` as well. As is the case 10 | /// with `git status` command. 11 | public let status: [Status] 12 | 13 | /// The differences between the file in HEAD and the file in the index. 14 | /// 15 | /// This represents the changes that have been staged but not committed. 16 | /// If the file is not staged, this is `nil`. 17 | public let index: Diff.Delta? 18 | 19 | /// The differences between the file in the index and the file in the working directory. 20 | /// 21 | /// This represents the changes on working tree that are not staged. 22 | /// If the file is staged and there is no additional changes, this is `nil`. 23 | public let workingTree: Diff.Delta? 24 | 25 | init(raw: git_status_entry) { 26 | status = Status.from(raw.status.rawValue) 27 | 28 | index = if let rawDelta = raw.head_to_index?.pointee { 29 | Diff.Delta(raw: rawDelta) 30 | } else { nil } 31 | 32 | workingTree = if let rawDelta = raw.index_to_workdir?.pointee { 33 | Diff.Delta(raw: rawDelta) 34 | } else { nil } 35 | } 36 | 37 | /// Represents the status of a file in the repository. 38 | /// 39 | /// This enumeration provides a detailed status of files in a Git repository. Each case corresponds to a specific 40 | /// status that a file can have in the repository, similar to the output of the `git status` command. 41 | public enum Status { 42 | /// The file is `tracked` and its content has no changes. 43 | case current 44 | 45 | /// The file is `untracked` and it is staged. 46 | /// 47 | /// This is a file that is in the index which is not tracked earlier by Git. 48 | case indexNew 49 | 50 | /// The file has been modified and the changes are staged. 51 | /// 52 | /// This is a `tracked` file that has changes and it's in the index. 53 | case indexModified 54 | 55 | /// The file has been deleted and this deletion is staged. 56 | /// 57 | /// This is a `tracked` file that is deleted and it's in the index. 58 | case indexDeleted 59 | 60 | /// The file has been renamed and this change is staged in the index. 61 | /// 62 | /// This is a `tracked` file that is renamed and staged. 63 | case indexRenamed 64 | 65 | /// The file type has changed (e.g., from regular file to symlink) and this change is staged in the index. 66 | /// 67 | /// This is a `tracked` file that has its type changed and staged. 68 | case indexTypeChange 69 | 70 | /// The file is new and `untracked` in the working directory. 71 | /// 72 | /// This is a file that is not staged yet and not tracked by Git. Newly created files are in this state. 73 | case workingTreeNew 74 | 75 | /// The file has been modified in the working directory but the changes are not yet staged. 76 | /// 77 | /// This is a `tracked` file that has changes and it's in the working tree. 78 | case workingTreeModified 79 | 80 | /// The file has been deleted from the working directory but the deletion is not yet staged. 81 | /// 82 | /// This is a `tracked` file that is deleted and it's in the working tree. 83 | case workingTreeDeleted 84 | 85 | /// The file has been renamed in the working directory but the change is not yet staged. 86 | /// 87 | /// This is a `tracked` file that is renamed and it's in the working tree. 88 | case workingTreeRenamed 89 | 90 | /// The file type has changed in the working directory but the change is not yet staged. 91 | /// 92 | /// This is a `tracked` file that has its type changed and it's in the working tree. 93 | case workingTreeTypeChange 94 | 95 | /// The file is unreadable in the working directory, possibly due to permissions or other issues. 96 | case workingTreeUnreadable 97 | 98 | /// The file is ignored by Git (typically specified in `.gitignore`). 99 | case ignored 100 | 101 | /// The file has conflicts, usually as a result of a merge or rebase operation. 102 | case conflicted 103 | 104 | static func from(_ flags: UInt32) -> [Status] { 105 | Status.statusMapping.filter { flags & $0.value.rawValue != 0 }.map(\.key) 106 | } 107 | } 108 | } 109 | 110 | private extension StatusEntry.Status { 111 | // We use this instead of direct dictionary because this makes sure the result is ordered. 112 | static let statusMapping: [(key: StatusEntry.Status, value: git_status_t)] = [ 113 | (.current, GIT_STATUS_CURRENT), 114 | 115 | (.indexNew, GIT_STATUS_INDEX_NEW), 116 | (.indexModified, GIT_STATUS_INDEX_MODIFIED), 117 | (.indexDeleted, GIT_STATUS_INDEX_DELETED), 118 | (.indexRenamed, GIT_STATUS_INDEX_RENAMED), 119 | (.indexTypeChange, GIT_STATUS_INDEX_TYPECHANGE), 120 | 121 | (.workingTreeNew, GIT_STATUS_WT_NEW), 122 | (.workingTreeModified, GIT_STATUS_WT_MODIFIED), 123 | (.workingTreeDeleted, GIT_STATUS_WT_DELETED), 124 | (.workingTreeTypeChange, GIT_STATUS_WT_TYPECHANGE), 125 | (.workingTreeRenamed, GIT_STATUS_WT_RENAMED), 126 | (.workingTreeUnreadable, GIT_STATUS_WT_UNREADABLE), 127 | 128 | (.ignored, GIT_STATUS_IGNORED), 129 | (.conflicted, GIT_STATUS_CONFLICTED) 130 | ] 131 | } 132 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Models/FileMode.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | public enum FileMode: UInt32, LibGit2RawRepresentable { 4 | case unreadable = 0_000_000 5 | case tree = 0_040_000 6 | case blob = 0_100_644 7 | case blobExecutable = 0_100_755 8 | case symlink = 0_120_000 9 | case commit = 0_160_000 10 | 11 | init(raw: git_filemode_t) { 12 | self = switch raw { 13 | case GIT_FILEMODE_UNREADABLE: 14 | .unreadable 15 | case GIT_FILEMODE_TREE: 16 | .tree 17 | case GIT_FILEMODE_BLOB: 18 | .blob 19 | case GIT_FILEMODE_BLOB_EXECUTABLE: 20 | .blobExecutable 21 | case GIT_FILEMODE_LINK: 22 | .symlink 23 | case GIT_FILEMODE_COMMIT: 24 | .commit 25 | default: 26 | .unreadable 27 | } 28 | } 29 | 30 | var raw: git_filemode_t { 31 | git_filemode_t(rawValue) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Models/Options/CheckoutOptions.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | // Only internal usage for now 4 | struct CheckoutOptions { 5 | /// The checkout strategy to use. Default is `.safe`. 6 | let strategy: CheckoutStrategy 7 | 8 | /// The paths to checkout. If empty, all paths will be checked out. 9 | let paths: [String] 10 | 11 | init(strategy: CheckoutStrategy = .safe, paths: [String] = []) { 12 | self.strategy = strategy 13 | self.paths = paths 14 | } 15 | 16 | func withGitCheckoutOptions(_ body: (git_checkout_options) throws -> T) rethrows -> T { 17 | // Initialize the options with the default values 18 | var options = git_checkout_options() 19 | // TODO: Throw an error if it fails 20 | git_checkout_options_init(&options, UInt32(GIT_CHECKOUT_OPTIONS_VERSION)) 21 | 22 | // Set the checkout strategies 23 | options.checkout_strategy = strategy.rawValue 24 | 25 | // Set the paths 26 | var checkoutPaths = paths.gitStrArray 27 | defer { git_strarray_free(&checkoutPaths) } 28 | 29 | options.paths = checkoutPaths 30 | 31 | return try body(options) 32 | } 33 | } 34 | 35 | struct CheckoutStrategy: OptionSet { 36 | // MARK: - Properties 37 | 38 | public let rawValue: UInt32 39 | 40 | // MARK: - Initializers 41 | 42 | init(_ strategy: git_checkout_strategy_t) { 43 | rawValue = strategy.rawValue 44 | } 45 | 46 | public init(rawValue: UInt32) { 47 | self.rawValue = rawValue 48 | } 49 | 50 | // MARK: - Options 51 | 52 | public static let none = CheckoutStrategy(GIT_CHECKOUT_NONE) 53 | 54 | public static let safe = CheckoutStrategy(GIT_CHECKOUT_SAFE) 55 | 56 | public static let force = CheckoutStrategy(GIT_CHECKOUT_FORCE) 57 | 58 | public static let recreateMissing = CheckoutStrategy(GIT_CHECKOUT_RECREATE_MISSING) 59 | 60 | public static let allowConflicts = CheckoutStrategy(GIT_CHECKOUT_ALLOW_CONFLICTS) 61 | 62 | public static let removeUntracked = CheckoutStrategy(GIT_CHECKOUT_REMOVE_UNTRACKED) 63 | 64 | public static let removeIgnored = CheckoutStrategy(GIT_CHECKOUT_REMOVE_IGNORED) 65 | 66 | public static let updateOnly = CheckoutStrategy(GIT_CHECKOUT_UPDATE_ONLY) 67 | 68 | public static let notUpdateIndex = CheckoutStrategy(GIT_CHECKOUT_DONT_UPDATE_INDEX) 69 | 70 | public static let noRefresh = CheckoutStrategy(GIT_CHECKOUT_NO_REFRESH) 71 | 72 | public static let skipUnmerged = CheckoutStrategy(GIT_CHECKOUT_SKIP_UNMERGED) 73 | 74 | public static let useOurs = CheckoutStrategy(GIT_CHECKOUT_USE_OURS) 75 | 76 | public static let useTheirs = CheckoutStrategy(GIT_CHECKOUT_USE_THEIRS) 77 | 78 | public static let disablePathSpecMatch = CheckoutStrategy(GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH) 79 | 80 | public static let skipLockedDirectories = CheckoutStrategy(GIT_CHECKOUT_SKIP_LOCKED_DIRECTORIES) 81 | 82 | // TODO: Add remaining options 83 | } 84 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Models/Options/CloneOptions.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | /// Options for the clone operation. 4 | public struct CloneOptions { 5 | public static let `default` = CloneOptions() 6 | 7 | public static let bare = CloneOptions(bare: true) 8 | 9 | /// If true, clone as a bare repository. Otherwise, clone as a normal repository. Default is false. 10 | public let bare: Bool 11 | 12 | public init(bare: Bool = false) { 13 | self.bare = bare 14 | } 15 | 16 | var gitCloneOptions: git_clone_options { 17 | var options = git_clone_options() 18 | git_clone_init_options(&options, UInt32(GIT_CLONE_OPTIONS_VERSION)) 19 | 20 | options.bare = bare ? 1 : 0 21 | 22 | return options 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Models/Options/DiffOption.swift: -------------------------------------------------------------------------------- 1 | public struct DiffOption: OptionSet { 2 | public let rawValue: Int 3 | 4 | public init(rawValue: Int) { 5 | self.rawValue = rawValue 6 | } 7 | 8 | public static let workingTree = DiffOption(rawValue: 1 << 0) 9 | public static let index = DiffOption(rawValue: 1 << 1) 10 | } 11 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Models/Options/LogSortingOption.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | /// Options for sorting the log. 4 | public struct LogSortingOption: OptionSet, Sendable { 5 | // MARK: - Properties 6 | 7 | public let rawValue: UInt32 8 | 9 | // MARK: - Initializers 10 | 11 | init(_ sortType: git_sort_t) { 12 | rawValue = sortType.rawValue 13 | } 14 | 15 | public init(rawValue: UInt32) { 16 | self.rawValue = rawValue 17 | } 18 | 19 | // MARK: - Options 20 | 21 | /// No sorting. 22 | public static let none = LogSortingOption(GIT_SORT_NONE) 23 | 24 | /// Sort by commit time. 25 | public static let time = LogSortingOption(GIT_SORT_TIME) 26 | 27 | /// Sort by topological order. 28 | public static let topological = LogSortingOption(GIT_SORT_TOPOLOGICAL) 29 | 30 | /// Sort by reverse. 31 | public static let reverse = LogSortingOption(GIT_SORT_REVERSE) 32 | } 33 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Models/Options/ResetOption.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | /// Options for reset operation. 4 | public enum ResetOption: LibGit2RawRepresentable { 5 | /// Does not touch the index file or the working tree at all 6 | /// (but resets the head to `commit`, just like all modes do). 7 | case soft 8 | 9 | /// Resets the index but not the working tree. 10 | /// (i.e., the changed files are preserved but not marked for commit) 11 | case mixed 12 | 13 | /// Resets the index and working tree. Any changes to tracked files in the working tree 14 | /// since `commit` are discarded. Any untracked files or directories in the way of 15 | /// writing any tracked files are simply deleted. 16 | case hard 17 | 18 | init(raw: git_reset_t) { 19 | self = switch raw { 20 | case GIT_RESET_SOFT: 21 | .soft 22 | case GIT_RESET_MIXED: 23 | .mixed 24 | case GIT_RESET_HARD: 25 | .hard 26 | default: 27 | .soft 28 | } 29 | } 30 | 31 | var raw: git_reset_t { 32 | switch self { 33 | case .soft: 34 | GIT_RESET_SOFT 35 | case .mixed: 36 | GIT_RESET_MIXED 37 | case .hard: 38 | GIT_RESET_HARD 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Models/Options/RestoreOption.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | /// Options for restoring. 4 | public struct RestoreOption: OptionSet, Sendable { 5 | // MARK: - Properties 6 | 7 | public let rawValue: UInt32 8 | 9 | // MARK: - Initializers 10 | 11 | public init(rawValue: UInt32) { 12 | self.rawValue = rawValue 13 | } 14 | 15 | // MARK: - Options 16 | 17 | /// Restore the working tree. 18 | public static let workingTree = RestoreOption(rawValue: 1 << 0) 19 | 20 | /// Restore the index. 21 | public static let staged = RestoreOption(rawValue: 1 << 1) 22 | } 23 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Models/Options/StashOption.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | /// Options for stashing changes. 4 | public struct StashOption: OptionSet, Sendable { 5 | // MARK: - Properties 6 | 7 | public let rawValue: UInt32 8 | 9 | // MARK: - Initializers 10 | 11 | init(_ stashFlag: git_stash_flags) { 12 | rawValue = stashFlag.rawValue 13 | } 14 | 15 | public init(rawValue: UInt32) { 16 | self.rawValue = rawValue 17 | } 18 | 19 | // MARK: - Options 20 | 21 | /// No option, default behavior. 22 | public static let `default` = StashOption(GIT_STASH_DEFAULT) 23 | 24 | /// All changes already added to the index are left intact. 25 | public static let keepIndex = StashOption(GIT_STASH_KEEP_INDEX) 26 | 27 | /// All untracked files are also stashed. 28 | public static let includeUntracked = StashOption(GIT_STASH_INCLUDE_UNTRACKED) 29 | 30 | /// All ignored files are also stashed. 31 | public static let includeIgnored = StashOption(GIT_STASH_INCLUDE_IGNORED) 32 | 33 | /// All ignored and untracked files are also stashed. 34 | public static let keepAll = StashOption(GIT_STASH_KEEP_ALL) 35 | } 36 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Models/Options/StatusOption.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | /// Options for the status operation. 4 | public struct StatusOption: OptionSet, Sendable { 5 | // MARK: - Properties 6 | 7 | public let rawValue: UInt32 8 | 9 | // MARK: - Initializers 10 | 11 | init(_ statusFlag: git_status_opt_t) { 12 | rawValue = statusFlag.rawValue 13 | } 14 | 15 | public init(rawValue: UInt32) { 16 | self.rawValue = rawValue 17 | } 18 | 19 | public static let `default`: StatusOption = [.includeUntracked, .recurseUntrackedDirectories] 20 | 21 | // TODO: Add documentation considering the libgit2 documentation of git_status_opt_t 22 | 23 | public static let includeUntracked = StatusOption(GIT_STATUS_OPT_INCLUDE_UNTRACKED) 24 | 25 | public static let includeIgnored = StatusOption(GIT_STATUS_OPT_INCLUDE_IGNORED) 26 | 27 | public static let includeUnmodified = StatusOption(GIT_STATUS_OPT_INCLUDE_UNMODIFIED) 28 | 29 | public static let excludeSubmodules = StatusOption(GIT_STATUS_OPT_EXCLUDE_SUBMODULES) 30 | 31 | public static let recurseUntrackedDirectories = StatusOption(GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS) 32 | 33 | public static let disablePathSpecMatch = StatusOption(GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH) 34 | 35 | public static let recurseIgnoredDirectories = StatusOption(GIT_STATUS_OPT_RECURSE_IGNORED_DIRS) 36 | 37 | public static let renamesIndex = StatusOption(GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX) 38 | 39 | public static let renamesWorkingTree = StatusOption(GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR) 40 | 41 | public static let sortCaseSensitively = StatusOption(GIT_STATUS_OPT_SORT_CASE_SENSITIVELY) 42 | 43 | public static let sortCaseInsensitively = StatusOption(GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY) 44 | 45 | public static let renamesFromRewrites = StatusOption(GIT_STATUS_OPT_RENAMES_FROM_REWRITES) 46 | 47 | public static let noRefresh = StatusOption(GIT_STATUS_OPT_NO_REFRESH) 48 | 49 | public static let updateIndex = StatusOption(GIT_STATUS_OPT_UPDATE_INDEX) 50 | 51 | public static let includeUnreadable = StatusOption(GIT_STATUS_OPT_INCLUDE_UNREADABLE) 52 | 53 | public static let includeUnreadableAsUntracked = StatusOption(GIT_STATUS_OPT_INCLUDE_UNREADABLE_AS_UNTRACKED) 54 | } 55 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Models/Progress/CheckoutProgress.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibrahimcetin/SwiftGitX/e190291c8b8ff5732feb3c75348ce3c680df2210/Sources/SwiftGitX/Models/Progress/CheckoutProgress.swift -------------------------------------------------------------------------------- /Sources/SwiftGitX/Models/Progress/TransferProgress.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | public typealias TransferProgressHandler = (TransferProgress) -> Void 4 | 5 | public struct TransferProgress { 6 | /// The number of objects that the pack will contain. 7 | public let totalObjects: Int 8 | 9 | /// The number of objects that the pack has indexed. 10 | public let indexedObjects: Int 11 | 12 | /// The number of objects that the pack has received. 13 | public let receivedObjects: Int 14 | 15 | /// The number of local objects that the pack has. 16 | public let localObjects: Int 17 | 18 | /// The number of deltas that the pack will contain. 19 | public let totalDeltas: Int 20 | 21 | /// The number of bytes that the pack has indexed. 22 | public let indexedDeltas: Int 23 | 24 | /// The number of bytes that the pack has received. 25 | public let receivedBytes: Int 26 | 27 | init(from stats: git_indexer_progress) { 28 | totalObjects = Int(stats.total_objects) 29 | indexedObjects = Int(stats.indexed_objects) 30 | receivedObjects = Int(stats.received_objects) 31 | localObjects = Int(stats.local_objects) 32 | totalDeltas = Int(stats.total_deltas) 33 | indexedDeltas = Int(stats.indexed_deltas) 34 | receivedBytes = Int(stats.received_bytes) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Models/Remote.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import libgit2 3 | 4 | public enum RemoteError: Error, Equatable { 5 | case invalid(String) 6 | case notFound(String) 7 | case failedToConnect(String) 8 | case unableToGetBranches(String) 9 | } 10 | 11 | /// A remote representation in the repository. 12 | public struct Remote: Equatable, Hashable { 13 | /// The name of the remote. 14 | public let name: String 15 | 16 | /// The URL of the remote. 17 | public let url: URL 18 | 19 | /// The branches of the remote which are available in the repository. 20 | public var branches: [Branch] { 21 | let branchSequence = BranchSequence(type: .remote, repositoryPointer: repositoryPointer) 22 | return branchSequence.filter { 23 | $0.fullName.hasPrefix("\(GitDirectoryConstants.remotes)\(name)/") 24 | } 25 | } 26 | 27 | /// The opaque pointer to the repository. 28 | private let repositoryPointer: OpaquePointer 29 | 30 | /// Initializes a `Remote` instance with the given opaque pointer. 31 | /// 32 | /// - Parameter pointer: The opaque pointer representing the remote. 33 | /// - Throws: A `RemoteError` if the remote is invalid or if the URLs are invalid. 34 | init(pointer: OpaquePointer) throws { 35 | // Get the remote name, URL, and push URL 36 | let name = git_remote_name(pointer) 37 | let url = git_remote_url(pointer) 38 | 39 | let repositoryPointer = git_remote_owner(pointer) 40 | 41 | // Check if the remote name and url pointers are valid 42 | guard let name, let url, let repositoryPointer else { 43 | let errorMessage = String(cString: git_error_last().pointee.message) 44 | throw RemoteError.invalid(errorMessage) 45 | } 46 | 47 | // Set the name 48 | self.name = String(cString: name) 49 | 50 | // Check if the URL is valid 51 | guard let url = URL(string: String(cString: url)) else { 52 | throw RemoteError.invalid("Invalid URL") 53 | } 54 | 55 | self.url = url 56 | 57 | self.repositoryPointer = repositoryPointer 58 | } 59 | } 60 | 61 | // MARK: - Remote Extension 62 | 63 | public extension Remote { 64 | static func == (lhs: Remote, rhs: Remote) -> Bool { 65 | lhs.name == rhs.name && lhs.url == rhs.url 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Models/Signature.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import libgit2 3 | 4 | public enum SignatureError: Error { 5 | case invalid(String) 6 | case notFound(String) 7 | } 8 | 9 | // ? Can we use LibGit2RawRepresentable here? 10 | /// A signature representation in the repository. 11 | public struct Signature: Equatable, Hashable { 12 | /// The full name of the author. 13 | public let name: String 14 | 15 | /// The email of the author. 16 | public let email: String 17 | 18 | /// The date of the action happened. 19 | public let date: Date 20 | 21 | /// The timezone of the author. 22 | public let timezone: TimeZone 23 | 24 | init(raw: git_signature) { 25 | name = String(cString: raw.name) 26 | email = String(cString: raw.email) 27 | date = Date(timeIntervalSince1970: TimeInterval(raw.when.time)) 28 | timezone = TimeZone(secondsFromGMT: Int(raw.when.offset) * 60) ?? TimeZone.current 29 | } 30 | } 31 | 32 | public extension Signature { 33 | static func `default`(in repositoryPointer: OpaquePointer) throws -> Signature { 34 | var signature: UnsafeMutablePointer? 35 | defer { git_signature_free(signature) } 36 | 37 | let status = git_signature_default(&signature, repositoryPointer) 38 | 39 | guard let signature = signature?.pointee else { 40 | let errorMessage = String(cString: git_error_last().pointee.message) 41 | 42 | switch status { 43 | case GIT_ENOTFOUND.rawValue: 44 | throw SignatureError.notFound(errorMessage) 45 | default: 46 | throw SignatureError.invalid(errorMessage) 47 | } 48 | } 49 | 50 | return Signature(raw: signature) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Models/StashEntry.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A stash entry representation in the repository. 4 | public struct StashEntry: Equatable, Hashable { 5 | /// The index of the entry. 6 | public let index: Int 7 | 8 | /// The target commit of the entry. 9 | public let target: Commit 10 | 11 | /// The message associated with the entry. 12 | public let message: String 13 | 14 | /// The signature of the stasher. 15 | public let stasher: Signature 16 | 17 | /// The date of the stash entry. 18 | public let date: Date 19 | } 20 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Models/Types/BranchType.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | public enum BranchType: LibGit2RawRepresentable { 4 | case all 5 | case local 6 | case remote 7 | 8 | init(raw: git_branch_t) { 9 | switch raw { 10 | case GIT_BRANCH_ALL: 11 | self = .all 12 | case GIT_BRANCH_LOCAL: 13 | self = .local 14 | case GIT_BRANCH_REMOTE: 15 | self = .remote 16 | default: 17 | self = .all 18 | } 19 | } 20 | 21 | var raw: git_branch_t { 22 | switch self { 23 | case .all: 24 | GIT_BRANCH_ALL 25 | case .local: 26 | GIT_BRANCH_LOCAL 27 | case .remote: 28 | GIT_BRANCH_REMOTE 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Models/Types/ObjectType.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | public enum ObjectType: LibGit2RawRepresentable { 4 | case any 5 | case invalid 6 | case commit 7 | case tree 8 | case blob 9 | case tag 10 | case offsetDelta 11 | case referenceDelta 12 | 13 | init(raw: git_object_t) { 14 | self = Self.objectTypeMapping.first(where: { $0.value == raw })?.key ?? .invalid 15 | } 16 | 17 | var raw: git_object_t { 18 | ObjectType.objectTypeMapping[self] ?? GIT_OBJECT_INVALID 19 | } 20 | } 21 | 22 | private extension ObjectType { 23 | static let objectTypeMapping: [ObjectType: git_object_t] = [ 24 | .any: GIT_OBJECT_ANY, 25 | .invalid: GIT_OBJECT_INVALID, 26 | .commit: GIT_OBJECT_COMMIT, 27 | .tree: GIT_OBJECT_TREE, 28 | .blob: GIT_OBJECT_BLOB, 29 | .tag: GIT_OBJECT_TAG, 30 | .offsetDelta: GIT_OBJECT_OFS_DELTA, 31 | .referenceDelta: GIT_OBJECT_REF_DELTA 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Models/Types/TagType.swift: -------------------------------------------------------------------------------- 1 | public enum TagType { 2 | case annotated 3 | case lightweight 4 | } 5 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Objects/Blob.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import libgit2 3 | 4 | public enum BlobError: Error { 5 | case invalid(String) 6 | } 7 | 8 | /// A blob object representation in the repository. 9 | /// 10 | /// A blob object is a binary large object that stores the content of a file. 11 | public struct Blob: Object { 12 | /// The id of the blob. 13 | public let id: OID 14 | 15 | /// The content of the blob. 16 | public let content: Data 17 | 18 | /// The type of the object. 19 | public let type: ObjectType = .blob 20 | 21 | init(pointer: OpaquePointer) throws { 22 | let id = git_blob_id(pointer).pointee 23 | 24 | // ? Should we make it a computed property? 25 | let content = git_blob_rawcontent(pointer) 26 | 27 | guard let content else { 28 | let errorMessage = String(cString: git_error_last().pointee.message) 29 | throw BlobError.invalid(errorMessage) 30 | } 31 | 32 | self.id = OID(raw: id) 33 | self.content = Data(bytes: content, count: Int(git_blob_rawsize(pointer))) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Objects/Commit.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import libgit2 3 | 4 | public enum CommitError: Error { 5 | case invalid(String) 6 | case failedToGetParentCommits(String) 7 | } 8 | 9 | /// A commit object representation in the repository. 10 | public struct Commit: Object { 11 | /// The id of the commit. 12 | public let id: OID 13 | 14 | /// The author of the commit. 15 | public let author: Signature 16 | 17 | /// The committer of the commit. 18 | public let committer: Signature 19 | 20 | /// The message of the commit. 21 | public let message: String 22 | 23 | /// The first paragraph of the message. 24 | public let summary: String 25 | 26 | /// The message of the commit, excluding the first paragraph. 27 | public let body: String? 28 | 29 | /// The date of the commit. 30 | public let date: Date 31 | 32 | /// The tree of the commit. 33 | public let tree: Tree 34 | 35 | /// The parent commits of the commit. 36 | public var parents: [Commit] { 37 | get throws { 38 | // Lookup the commit 39 | let pointer = try ObjectFactory.lookupObjectPointer( 40 | oid: id.raw, 41 | type: GIT_OBJECT_COMMIT, 42 | repositoryPointer: repositoryPointer 43 | ) 44 | defer { git_commit_free(pointer) } 45 | 46 | // Get the parent commits 47 | var parents = [Commit]() 48 | let parentCount = git_commit_parentcount(pointer) 49 | 50 | for index in 0 ..< parentCount { 51 | var parentPointer: OpaquePointer? 52 | defer { git_commit_free(parentPointer) } 53 | 54 | let status = git_commit_parent(&parentPointer, pointer, index) 55 | 56 | guard let parentPointer, status == GIT_OK.rawValue else { 57 | let errorMessage = String(cString: git_error_last().pointee.message) 58 | throw CommitError.failedToGetParentCommits(errorMessage) 59 | } 60 | 61 | let parent = try Commit(pointer: parentPointer) 62 | parents.append(parent) 63 | } 64 | 65 | return parents 66 | } 67 | } 68 | 69 | /// The type of the object. 70 | public let type: ObjectType = .commit 71 | 72 | // This is necessary to get parents of the commit. 73 | private let repositoryPointer: OpaquePointer 74 | 75 | init(pointer: OpaquePointer) throws { 76 | let id = git_commit_id(pointer)?.pointee 77 | let author = git_commit_author(pointer) 78 | let committer = git_commit_committer(pointer) 79 | let message = git_commit_message(pointer) 80 | let body = git_commit_body(pointer) 81 | let summary = git_commit_summary(pointer) 82 | let date = git_commit_time(pointer) 83 | let repositoryPointer = git_commit_owner(pointer) 84 | 85 | var tree: OpaquePointer? 86 | defer { git_tree_free(tree) } 87 | 88 | let treeStatus = git_commit_tree(&tree, pointer) 89 | 90 | guard let id, let author = author?.pointee, let committer = committer?.pointee, let message, let summary, 91 | let tree, let repositoryPointer, 92 | treeStatus == GIT_OK.rawValue 93 | else { 94 | let errorMessage = String(cString: git_error_last().pointee.message) 95 | throw CommitError.invalid(errorMessage) 96 | } 97 | 98 | self.id = OID(raw: id) 99 | self.author = Signature(raw: author) 100 | self.committer = Signature(raw: committer) 101 | self.message = String(cString: message) 102 | self.body = if let body { String(cString: body) } else { nil } 103 | self.summary = String(cString: summary) 104 | self.date = Date(timeIntervalSince1970: TimeInterval(date)) 105 | self.tree = try Tree(pointer: tree) 106 | 107 | self.repositoryPointer = repositoryPointer 108 | } 109 | } 110 | 111 | public extension Commit { 112 | // To ignore repositoryPointer 113 | static func == (lhs: Commit, rhs: Commit) -> Bool { 114 | lhs.id == rhs.id && 115 | lhs.author == rhs.author && 116 | lhs.committer == rhs.committer && 117 | lhs.message == rhs.message && 118 | lhs.summary == rhs.summary && 119 | lhs.body == rhs.body && 120 | lhs.date == rhs.date && 121 | lhs.tree == rhs.tree 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Objects/OID.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | public enum OIDError: Error { 4 | case invalid(String) 5 | } 6 | 7 | /// An Object ID representation in the repository. 8 | /// 9 | /// The OID is a unique 40-byte length hex string that an object in the repository is identified with. 10 | /// Commits, trees, blobs, and tags all have an OID. 11 | /// 12 | /// You can also get an abbreviated version of the OID which is an 8-byte length hex string. 13 | public struct OID: LibGit2RawRepresentable { 14 | /// The zero (null) OID. 15 | public static let zero: OID = .init(raw: git_oid(id: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))) 16 | 17 | /// The libgit2 git_oid struct that this OID wraps. 18 | let raw: git_oid 19 | 20 | /// The 40-byte length hex string. 21 | /// 22 | /// This is the string representation of the OID. 23 | public var hex: String { 24 | hex(length: 40) 25 | } 26 | 27 | /// The 8-byte length hex string. 28 | /// 29 | /// This is the abbreviated string representation of the OID. 30 | public var abbreviated: String { 31 | hex(length: 8) 32 | } 33 | 34 | /// Create an OID from a git_oid. 35 | /// 36 | /// - Parameter oid: The git_oid. 37 | init(raw: git_oid) { 38 | self.raw = raw 39 | } 40 | 41 | /// Create an OID from a hex string. 42 | /// 43 | /// - Parameter hex: The 40-byte length hex string. 44 | public init(hex: String) throws { 45 | var raw = git_oid() 46 | let status = git_oid_fromstr(&raw, hex) 47 | 48 | guard status == GIT_OK.rawValue else { 49 | let errorMessage = String(cString: git_error_last().pointee.message) 50 | throw OIDError.invalid(errorMessage) 51 | } 52 | 53 | self.raw = raw 54 | } 55 | 56 | private func hex(length: Int) -> String { 57 | var oid = raw 58 | 59 | let bufferLength = length + 1 // +1 for \0 terminator 60 | var buffer = [Int8](repeating: 0, count: bufferLength) 61 | 62 | git_oid_tostr(&buffer, bufferLength, &oid) 63 | 64 | return String(cString: buffer) 65 | } 66 | } 67 | 68 | public extension OID { 69 | static func == (lhs: OID, rhs: OID) -> Bool { 70 | var left = lhs.raw 71 | var right = rhs.raw 72 | 73 | return git_oid_cmp(&left, &right) == 0 74 | } 75 | 76 | func hash(into hasher: inout Hasher) { 77 | withUnsafeBytes(of: raw.id) { hasher.combine(bytes: $0) } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Objects/Object.swift: -------------------------------------------------------------------------------- 1 | public enum ObjectError: Error { 2 | case invalid(String) 3 | } 4 | 5 | /// An object representation that can be stored in a Git repository. 6 | public protocol Object: Identifiable, Equatable, Hashable { 7 | /// The id of the object. 8 | var id: OID { get } 9 | 10 | /// The type of the object. 11 | var type: ObjectType { get } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Objects/Tree.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | public enum TreeError: Error { 4 | case invalid(String) 5 | } 6 | 7 | /// A tree representation in the repository. 8 | /// 9 | /// A tree object is a directory listing. It contains a list of entries, each of which contains a SHA-1 reference to a 10 | /// blob or subtree with its associated mode, type, and filename. 11 | /// 12 | /// Trees are used to represent the contents of a directory. They are also used to represent the contents of a commit. 13 | /// 14 | /// Trees are similar to directories in a filesystem, but they are stored as a single file in the Git repository. 15 | public struct Tree: Object { 16 | /// The id of the tree. 17 | public let id: OID 18 | 19 | /// The entries in the tree. 20 | public let entries: [Entry] 21 | 22 | /// The type of the object. 23 | public let type: ObjectType = .tree 24 | 25 | init(pointer: OpaquePointer) throws { 26 | // Get the id of the tree 27 | let id = git_tree_id(pointer) 28 | 29 | guard let id = id?.pointee else { 30 | let errorMessage = String(cString: git_error_last().pointee.message) 31 | throw TreeError.invalid(errorMessage) 32 | } 33 | 34 | // Get the number of entries in the tree 35 | let entryCount = git_tree_entrycount(pointer) 36 | 37 | // Get all the entries in the tree 38 | var entries = [Entry]() 39 | 40 | for index in 0 ..< entryCount { 41 | let entryPointer = git_tree_entry_byindex(pointer, index) 42 | 43 | guard let entryPointer else { 44 | throw TreeError.invalid("Invalid tree entry") 45 | } 46 | 47 | let entry = try Entry(pointer: entryPointer) 48 | entries.append(entry) 49 | } 50 | 51 | self.id = OID(raw: id) 52 | self.entries = entries 53 | } 54 | } 55 | 56 | public extension Tree { 57 | // ? Should we conform to Object? 58 | /// Represents an entry in a Git tree object. 59 | struct Entry: Identifiable, Equatable, Hashable { 60 | /// The OID of the object pointed to by the entry 61 | public let id: OID 62 | 63 | /// The filename of the entry 64 | public let name: String 65 | 66 | /// The type of the object pointed to by the entry (blob, tree or commit) 67 | public let type: ObjectType 68 | 69 | /// The file mode of the entry (permissions) 70 | public let mode: FileMode 71 | 72 | init(pointer: OpaquePointer) throws { 73 | let id = git_tree_entry_id(pointer) 74 | let name = git_tree_entry_name(pointer) 75 | let type = git_tree_entry_type(pointer) 76 | let mode = git_tree_entry_filemode(pointer) 77 | 78 | guard let id, let name else { 79 | throw TreeError.invalid("Invalid tree entry") 80 | } 81 | 82 | self.id = OID(raw: id.pointee) 83 | self.name = String(cString: name) 84 | self.type = ObjectType(raw: type) 85 | self.mode = FileMode(raw: mode) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/References/Branch.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | public enum BranchError: Error { 4 | case invalid(String) 5 | } 6 | 7 | /// A branch representation in the repository. 8 | public struct Branch: Reference { 9 | /// The target of the branch. 10 | public let target: any Object 11 | 12 | // ? Should we add `commit` property to get directly the commit object? 13 | 14 | /// The name of the branch. 15 | /// 16 | /// For example, `main` for a local branch and `origin/main` for a remote branch. 17 | public let name: String 18 | 19 | /// The full name of the branch. 20 | /// 21 | /// For example, `refs/heads/main` for a local branch and `refs/remotes/origin/main` for a remote branch. 22 | public let fullName: String 23 | 24 | /// The type of the branch. 25 | /// 26 | /// It can be either `local` or `remote`. 27 | public let type: BranchType 28 | 29 | /// The upstream branch of the branch. 30 | /// 31 | /// This property available for local branches only. 32 | public let upstream: (any Reference)? 33 | 34 | /// The upstream remote of the branch. 35 | /// 36 | /// This property available for both local and remote branches. 37 | public let remote: Remote? 38 | 39 | init(pointer: OpaquePointer) throws { 40 | let targetID = git_reference_target(pointer) 41 | let fullName = git_reference_name(pointer) 42 | let name = git_reference_shorthand(pointer) 43 | 44 | let repositoryPointer = git_reference_owner(pointer) 45 | 46 | guard let targetID = targetID?.pointee, let fullName, let name, let repositoryPointer else { 47 | let errorMessage = String(cString: git_error_last().pointee.message) 48 | throw BranchError.invalid(errorMessage) 49 | } 50 | 51 | // Get the target object of the branch. 52 | target = try ObjectFactory.lookupObject(oid: targetID, repositoryPointer: repositoryPointer) 53 | 54 | // Set the name of the branch. 55 | self.name = String(cString: name) 56 | self.fullName = String(cString: fullName) 57 | 58 | // Set the type of the branch. 59 | type = if self.fullName.hasPrefix(GitDirectoryConstants.heads) { 60 | .local 61 | } else if self.fullName.hasPrefix(GitDirectoryConstants.remotes) { 62 | .remote 63 | } else if self.fullName == "HEAD" { 64 | .local 65 | } else { 66 | // ? Should we throw an error here? 67 | throw BranchError.invalid("Invalid branch type") 68 | } 69 | 70 | // Get the upstream branch of the branch. 71 | var upstreamPointer: OpaquePointer? 72 | defer { git_reference_free(upstreamPointer) } 73 | 74 | let upstreamStatus = git_branch_upstream(&upstreamPointer, pointer) 75 | 76 | upstream = if let upstreamPointer, upstreamStatus == GIT_OK.rawValue { 77 | try Branch(pointer: upstreamPointer) 78 | } else { nil } 79 | 80 | // Get the remote of the branch. 81 | var remoteName = git_buf() 82 | defer { git_buf_free(&remoteName) } 83 | 84 | let remoteNameStatus = if type == .local { 85 | git_branch_upstream_remote(&remoteName, repositoryPointer, fullName) 86 | } else { 87 | git_branch_remote_name(&remoteName, repositoryPointer, fullName) 88 | } 89 | 90 | if let rawRemoteName = remoteName.ptr, remoteNameStatus == GIT_OK.rawValue { 91 | // Look up the remote. 92 | var remotePointer: OpaquePointer? 93 | defer { git_remote_free(remotePointer) } 94 | 95 | let remoteStatus = git_remote_lookup(&remotePointer, repositoryPointer, rawRemoteName) 96 | 97 | remote = if let remotePointer, remoteStatus == GIT_OK.rawValue { 98 | try Remote(pointer: remotePointer) 99 | } else { nil } 100 | } else { 101 | remote = nil 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/References/Reference.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | public enum ReferenceError: Error, Equatable { 4 | case invalid(String) 5 | case notFound 6 | } 7 | 8 | /// A reference representation in a Git repository. 9 | public protocol Reference: Equatable, Hashable { 10 | /// The target of the reference. 11 | var target: any Object { get } 12 | 13 | /// The name of the reference. 14 | /// 15 | /// For example, `main`. 16 | var name: String { get } 17 | 18 | /// The full name of the reference. 19 | /// 20 | /// For example, `refs/heads/main`. 21 | var fullName: String { get } 22 | } 23 | 24 | public extension Reference { 25 | static func == (lhs: Self, rhs: Self) -> Bool { 26 | lhs.target.id == rhs.target.id && lhs.name == rhs.name && lhs.fullName == rhs.fullName 27 | } 28 | 29 | func hash(into hasher: inout Hasher) { 30 | hasher.combine(target.id) 31 | hasher.combine(name) 32 | hasher.combine(fullName) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/References/Tag.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import libgit2 3 | 4 | public enum TagError: Error { 5 | case invalid(String) 6 | } 7 | 8 | /// A tag representation in the repository. 9 | public struct Tag: Object, Reference { 10 | /// The id of the tag. 11 | public let id: OID 12 | 13 | /// The target of the tag. 14 | public let target: any Object 15 | 16 | /// The name of the tag. 17 | /// 18 | /// For example, `v1.0.0`. 19 | public let name: String 20 | 21 | /// The full name of the tag. 22 | /// 23 | /// For example, `refs/tags/v1.0.0`. 24 | public let fullName: String 25 | 26 | /// The tagger of the tag. 27 | /// 28 | /// If the tag is lightweight, the tagger will be `nil`. 29 | public let tagger: Signature? 30 | 31 | /// The message of the tag. 32 | public let message: String? 33 | 34 | /// The type of the object. 35 | public let type: ObjectType = .tag 36 | 37 | init(pointer: OpaquePointer) throws { 38 | // Get the id of the tag. 39 | let id = git_tag_id(pointer) 40 | 41 | // Get the target id of the tag. 42 | let targetID = git_tag_target_id(pointer) 43 | 44 | // Get the name of the tag. 45 | let name = git_tag_name(pointer) 46 | 47 | // Get the tagger of the tag. 48 | let tagger = git_tag_tagger(pointer) 49 | 50 | // Get the message of the tag. 51 | let message = git_tag_message(pointer) 52 | 53 | // Get the repository pointer. 54 | let repositoryPointer = git_tag_owner(pointer) 55 | 56 | guard let id = id?.pointee, let targetID = targetID?.pointee, let name, let repositoryPointer else { 57 | let errorMessage = String(cString: git_error_last().pointee.message) 58 | throw TagError.invalid(errorMessage) 59 | } 60 | 61 | // Set the id of the tag. 62 | self.id = OID(raw: id) 63 | 64 | // Set the target of the tag. 65 | target = try ObjectFactory.lookupObject(oid: targetID, repositoryPointer: repositoryPointer) 66 | 67 | // Set the name of the tag. 68 | self.name = String(cString: name) 69 | fullName = GitDirectoryConstants.tags + self.name 70 | 71 | // Set the tagger of the tag. 72 | self.tagger = if let tagger = tagger?.pointee { 73 | Signature(raw: tagger) 74 | } else { nil } 75 | 76 | // Set the message of the tag. If the message is empty, set it to `nil`. 77 | self.message = if let message, strcmp(message, "") != 0 { 78 | String(cString: message) 79 | } else { nil } 80 | } 81 | 82 | /// Lightweight tag initializer 83 | init(name: String, target: any Object) { 84 | id = target.id 85 | 86 | self.target = target 87 | 88 | self.name = name 89 | fullName = GitDirectoryConstants.tags + name 90 | 91 | tagger = nil 92 | message = nil 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Sequences/BranchSequence.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | public struct BranchSequence: Sequence { 4 | let type: BranchType 5 | 6 | private let repositoryPointer: OpaquePointer 7 | 8 | init(type: BranchType, repositoryPointer: OpaquePointer) { 9 | self.type = type 10 | self.repositoryPointer = repositoryPointer 11 | } 12 | 13 | public func makeIterator() -> BranchIterator { 14 | BranchIterator(type: type, repositoryPointer: repositoryPointer) 15 | } 16 | } 17 | 18 | public class BranchIterator: IteratorProtocol { 19 | public let type: BranchType 20 | 21 | private var branchIterator: OpaquePointer? 22 | private let repositoryPointer: OpaquePointer 23 | 24 | init(type: BranchType, repositoryPointer: OpaquePointer) { 25 | self.type = type 26 | self.repositoryPointer = repositoryPointer 27 | 28 | // Create a branch iterator 29 | git_branch_iterator_new(&branchIterator, repositoryPointer, type.raw) 30 | } 31 | 32 | deinit { 33 | git_branch_iterator_free(branchIterator) 34 | } 35 | 36 | public func next() -> Branch? { 37 | var branchPointer: OpaquePointer? 38 | var type = type.raw 39 | 40 | while true { 41 | // Task should not be cancelled 42 | if Task.isCancelled { return nil } 43 | 44 | // Get the next branch 45 | let status = git_branch_next(&branchPointer, &type, branchIterator) 46 | defer { git_reference_free(branchPointer) } 47 | 48 | // Check if the status is ITEROVER. If so, return nil 49 | if status == GIT_ITEROVER.rawValue { return nil } 50 | 51 | // Check if the branch pointer is not nil and the status is OK 52 | // If any error occurs, continue to the next iteration 53 | guard let branchPointer, status == GIT_OK.rawValue else { 54 | continue 55 | } 56 | 57 | // Try to create a branch from the pointer 58 | // If the reference is not valid, continue to the next iteration 59 | if let branch = try? Branch(pointer: branchPointer) { 60 | return branch 61 | } else { 62 | continue 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Sequences/CommitSequence.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | /// A sequence of commits. 4 | /// 5 | /// This sequence is an async sequence that iterates over the commits in a repository. 6 | /// 7 | /// - Warning: The sequence's task should be cancelled before ``Repository`` is deinitialized. 8 | public struct CommitSequence: Sequence { 9 | public typealias Element = Commit 10 | 11 | public let root: Commit 12 | public let sorting: LogSortingOption 13 | 14 | private let repositoryPointer: OpaquePointer 15 | 16 | init(root: Commit, sorting: LogSortingOption, repositoryPointer: OpaquePointer) { 17 | self.root = root 18 | self.sorting = sorting 19 | self.repositoryPointer = repositoryPointer 20 | } 21 | 22 | public func makeIterator() -> CommitIterator { 23 | CommitIterator(root: root, sorting: sorting, repositoryPointer: repositoryPointer) 24 | } 25 | } 26 | 27 | public class CommitIterator: IteratorProtocol { 28 | public let root: Commit 29 | public let sorting: LogSortingOption 30 | 31 | private let walkerPointer: OpaquePointer? 32 | private let repositoryPointer: OpaquePointer 33 | 34 | init(root: Commit, sorting: LogSortingOption, repositoryPointer: OpaquePointer) { 35 | self.root = root 36 | self.sorting = sorting 37 | 38 | self.repositoryPointer = repositoryPointer 39 | 40 | // Create a rev walker 41 | var walkerPointer: OpaquePointer? 42 | git_revwalk_new(&walkerPointer, repositoryPointer) 43 | 44 | self.walkerPointer = walkerPointer 45 | 46 | // Set the root commit 47 | var rootID = root.id.raw 48 | git_revwalk_push(walkerPointer, &rootID) 49 | 50 | // Set the sorting 51 | git_revwalk_sorting(walkerPointer, sorting.rawValue) 52 | } 53 | 54 | deinit { 55 | git_revwalk_free(walkerPointer) 56 | } 57 | 58 | public func next() -> Commit? { 59 | // Task should not be cancelled 60 | if Task.isCancelled { return nil } 61 | 62 | // Get the next commit 63 | var oid = git_oid() 64 | let status = git_revwalk_next(&oid, walkerPointer) 65 | 66 | // Check if the status is OK 67 | guard status == GIT_OK.rawValue else { 68 | return nil 69 | } 70 | 71 | return try? ObjectFactory.lookupObject(oid: oid, repositoryPointer: repositoryPointer) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Sequences/ReferenceIterator.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | public class ReferenceIterator: IteratorProtocol { 4 | private var referenceIterator: UnsafeMutablePointer? 5 | private let repositoryPointer: OpaquePointer 6 | 7 | init(glob: String? = nil, repositoryPointer: OpaquePointer) { 8 | self.repositoryPointer = repositoryPointer 9 | 10 | // Create a reference iterator 11 | if let glob { 12 | git_reference_iterator_glob_new(&referenceIterator, repositoryPointer, glob) 13 | } else { 14 | git_reference_iterator_new(&referenceIterator, repositoryPointer) 15 | } 16 | } 17 | 18 | deinit { 19 | git_reference_iterator_free(referenceIterator) 20 | } 21 | 22 | public func next() -> (any Reference)? { 23 | var referencePointer: OpaquePointer? 24 | 25 | while true { 26 | // Task should not be cancelled 27 | if Task.isCancelled { return nil } 28 | 29 | // Get the next reference 30 | let status = git_reference_next(&referencePointer, referenceIterator) 31 | defer { git_reference_free(referencePointer) } 32 | 33 | // Check if the status is ITEROVER. If so, return nil 34 | if status == GIT_ITEROVER.rawValue { return nil } 35 | 36 | // Check if the reference pointer is not nil and the status is OK 37 | // If any error occurs, continue to the next iteration 38 | guard let referencePointer, status == GIT_OK.rawValue else { 39 | continue 40 | } 41 | 42 | // Try to create a reference from the pointer 43 | // If the reference is not valid, continue to the next iteration 44 | if let reference = try? ReferenceFactory.makeReference(pointer: referencePointer) { 45 | return reference 46 | } else { 47 | continue 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Sequences/RemoteIterator.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | public struct RemoteIterator: IteratorProtocol { 4 | private let remoteNames: [String] 5 | private let repositoryPointer: OpaquePointer 6 | 7 | init(remoteNames: [String], repositoryPointer: OpaquePointer) { 8 | self.remoteNames = remoteNames 9 | self.repositoryPointer = repositoryPointer 10 | } 11 | 12 | private var index = 0 13 | 14 | public mutating func next() -> Remote? { 15 | while true { 16 | // Task should not be cancelled 17 | if Task.isCancelled { return nil } 18 | 19 | // Check if the index is out of bounds 20 | guard index < remoteNames.count else { return nil } 21 | 22 | defer { index += 1 } 23 | 24 | // Get the remote name 25 | let name = remoteNames[index] 26 | 27 | // Try to get the remote pointer 28 | guard let remotePointer = try? ReferenceFactory.lookupRemotePointer( 29 | name: name, 30 | repositoryPointer: repositoryPointer 31 | ) else { continue } 32 | defer { git_remote_free(remotePointer) } 33 | 34 | // Try to create a remote from the pointer 35 | // If the remote is not valid, continue to the next iteration 36 | if let remote = try? Remote(pointer: remotePointer) { 37 | return remote 38 | } else { 39 | continue 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Sequences/StashIterator.swift: -------------------------------------------------------------------------------- 1 | public struct StashIterator: IteratorProtocol { 2 | private var index = 0 3 | private let entries: [StashEntry] 4 | 5 | init(entries: [StashEntry]) { 6 | self.entries = entries 7 | } 8 | 9 | public mutating func next() -> StashEntry? { 10 | // Task should not be cancelled 11 | if Task.isCancelled { return nil } 12 | 13 | guard index < entries.count else { return nil } 14 | 15 | defer { index += 1 } 16 | 17 | return entries[index] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/Sequences/TagIterator.swift: -------------------------------------------------------------------------------- 1 | public class TagIterator: IteratorProtocol { 2 | private let referenceIterator: ReferenceIterator 3 | 4 | init(repositoryPointer: OpaquePointer) { 5 | referenceIterator = ReferenceIterator( 6 | glob: "\(GitDirectoryConstants.tags)*", 7 | repositoryPointer: repositoryPointer 8 | ) 9 | } 10 | 11 | public func next() -> Tag? { 12 | let reference = referenceIterator.next() 13 | 14 | return reference as? Tag 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/SwiftGitX.docc/Articles/GettingStarted.md: -------------------------------------------------------------------------------- 1 | # Getting Started with SwiftGitX 2 | 3 | @Metadata { 4 | @PageImage(purpose: icon, source: "Git-Icon-White") 5 | } 6 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/SwiftGitX.docc/Extensions/Repository.md: -------------------------------------------------------------------------------- 1 | # ``SwiftGitX/Repository`` 2 | 3 | ## Topics 4 | 5 | ### Creating a Repository 6 | 7 | - ``init(at:createIfNotExists:)`` 8 | - ``open(at:)`` 9 | - ``create(at:isBare:)`` 10 | - ``clone(from:to:options:)`` 11 | - ``clone(from:to:options:transferProgressHandler:)`` 12 | 13 | ### Properties 14 | 15 | - ``HEAD`` 16 | - ``workingDirectory`` 17 | - ``isHEADDetached`` 18 | - ``isHEADUnborn`` 19 | - ``isEmpty`` 20 | - ``isBare`` 21 | - ``isShallow`` 22 | 23 | ### Create a Commit 24 | 25 | - ``add(file:)`` 26 | - ``add(files:)`` 27 | - ``add(path:)`` 28 | - ``add(paths:)`` 29 | - ``commit(message:)`` 30 | 31 | ### Collections 32 | 33 | - ``branch`` 34 | - ``config-swift.property`` 35 | - ``config-swift.type.property`` 36 | - ``reference`` 37 | - ``remote`` 38 | - ``stash`` 39 | - ``tag`` 40 | 41 | ### Diff 42 | 43 | - ``diff(to:)`` 44 | - ``diff(commit:)`` 45 | - ``diff(from:to:)`` 46 | 47 | ### Log 48 | 49 | - ``log(sorting:)`` 50 | - ``log(from:sorting:)-2c8fu`` 51 | - ``log(from:sorting:)-3rteq`` 52 | - ``LogSortingOption`` 53 | 54 | ### Patch 55 | 56 | - ``patch(from:)`` 57 | - ``patch(from:to:)-10g8i`` 58 | - ``patch(from:to:)-957bd`` 59 | 60 | ### Restore 61 | 62 | - ``restore(_:files:)`` 63 | - ``restore(_:paths:)`` 64 | - ``RestoreOption`` 65 | 66 | ### Reset 67 | 68 | - ``reset(to:mode:)`` 69 | - ``ResetOption`` 70 | 71 | - ``reset(from:files:)`` 72 | - ``reset(from:paths:)`` 73 | 74 | ### Revert 75 | 76 | - ``revert(_:)`` 77 | 78 | ### Show 79 | 80 | - ``show(id:)`` 81 | 82 | ### Status 83 | 84 | - ``status(options:)`` 85 | 86 | #### Status of a specific file 87 | 88 | - ``status(file:)`` 89 | - ``status(path:)`` 90 | 91 | ### Switch 92 | 93 | - ``switch(to:)-8oxzx`` 94 | - ``switch(to:)-16nyq`` 95 | - ``switch(to:)-2ysnq`` 96 | 97 | ### Remote Repository Operations 98 | 99 | - ``push(remote:)`` 100 | - ``fetch(remote:)`` 101 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/SwiftGitX.docc/Extensions/SwiftGitX-Extension.md: -------------------------------------------------------------------------------- 1 | # ``SwiftGitX/SwiftGitX`` 2 | 3 | ## Topics 4 | 5 | ### Initializing SwiftGitX 6 | 7 | - ``initialize()`` 8 | 9 | ### Shutdown 10 | 11 | - ``shutdown()`` 12 | 13 | ### Libgit2 14 | 15 | - ``libgit2Version`` 16 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/SwiftGitX.docc/Hierarchy/Collections.md: -------------------------------------------------------------------------------- 1 | # Collections of Git Commands 2 | 3 | ## Overview 4 | 5 | ## Topics 6 | 7 | - ``BranchCollection`` 8 | - ``ConfigCollection`` 9 | - ``ReferenceCollection`` 10 | - ``RemoteCollection`` 11 | - ``StashCollection`` 12 | - ``TagCollection`` 13 | 14 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/SwiftGitX.docc/Hierarchy/ErrorTypes.md: -------------------------------------------------------------------------------- 1 | # Error Types 2 | 3 | The error types 4 | 5 | ## Overview 6 | 7 | ## Topics 8 | 9 | ### Essential Errors 10 | 11 | - ``SwiftGitXError`` 12 | - ``RepositoryError`` 13 | 14 | ### Object Errors 15 | 16 | - ``CommitError`` 17 | - ``TreeError`` 18 | - ``BlobError`` 19 | - ``TagError`` 20 | - ``ObjectError`` 21 | - ``OIDError`` 22 | 23 | ### Reference Errors 24 | 25 | - ``BranchError`` 26 | - ``TagError`` 27 | - ``ReferenceError`` 28 | 29 | ### Collection Errors 30 | 31 | - ``BranchCollectionError`` 32 | - ``ReferenceCollectionError`` 33 | - ``RemoteCollectionError`` 34 | - ``StashCollectionError`` 35 | - ``TagCollectionError`` 36 | - ``IndexError`` 37 | 38 | ### Git Model Errors 39 | 40 | - ``RemoteError`` 41 | - ``SignatureError`` 42 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/SwiftGitX.docc/Hierarchy/GitModels.md: -------------------------------------------------------------------------------- 1 | # Git Models 2 | 3 | ## Overview 4 | 5 | ## Topics 6 | 7 | ### Diff 8 | 9 | - ``Diff`` 10 | - ``Patch`` 11 | - ``StatusEntry`` 12 | 13 | ### Remote 14 | 15 | - ``Remote`` 16 | 17 | ### Signature 18 | 19 | - ``Signature`` 20 | 21 | ### Stash 22 | 23 | - ``StashEntry`` 24 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/SwiftGitX.docc/Hierarchy/HelperModels.md: -------------------------------------------------------------------------------- 1 | # Helper Models 2 | 3 | ## Overview 4 | 5 | ## Topics 6 | 7 | ### Options 8 | 9 | - ``CloneOptions`` 10 | - ``DiffOption`` 11 | - ``LogSortingOption`` 12 | - ``ResetOption`` 13 | - ``RestoreOption`` 14 | - ``StashOption`` 15 | - ``StatusOption`` 16 | 17 | ### Types 18 | 19 | - ``BranchType`` 20 | - ``ObjectType`` 21 | - ``TagType`` 22 | 23 | ### Progress 24 | 25 | - ``TransferProgress`` 26 | - ``TransferProgressHandler`` 27 | 28 | ### File Mode 29 | 30 | - ``FileMode`` 31 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/SwiftGitX.docc/Hierarchy/Iterators.md: -------------------------------------------------------------------------------- 1 | # Iterate Over Git Types 2 | 3 | ## Overview 4 | 5 | ## Topics 6 | 7 | ### Sequences 8 | 9 | - ``BranchSequence`` 10 | - ``CommitSequence`` 11 | 12 | ### Iterators 13 | 14 | - ``BranchIterator`` 15 | - ``CommitIterator`` 16 | - ``ReferenceIterator`` 17 | - ``RemoteIterator`` 18 | - ``StashIterator`` 19 | - ``TagIterator`` 20 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/SwiftGitX.docc/Hierarchy/Objects.md: -------------------------------------------------------------------------------- 1 | # Git Object Types 2 | 3 | The git object types in a repository. 4 | 5 | ## Overview 6 | 7 | ## Topics 8 | 9 | ### Object Types 10 | 11 | - ``Commit`` 12 | - ``Tree`` 13 | - ``Blob`` 14 | - ``Tag`` 15 | 16 | ### Object Protocol 17 | 18 | - ``Object`` 19 | 20 | ### Object Identifier 21 | 22 | - ``OID`` 23 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/SwiftGitX.docc/Hierarchy/References.md: -------------------------------------------------------------------------------- 1 | # Git Reference Types 2 | 3 | The git reference types 4 | 5 | ## Overview 6 | 7 | ## Topics 8 | 9 | ### Reference Types 10 | 11 | - ``Branch`` 12 | - ``Tag`` 13 | 14 | ### Reference Protocol 15 | 16 | - ``Reference`` 17 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/SwiftGitX.docc/Resources/Git-Icon-White.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibrahimcetin/SwiftGitX/e190291c8b8ff5732feb3c75348ce3c680df2210/Sources/SwiftGitX/SwiftGitX.docc/Resources/Git-Icon-White.png -------------------------------------------------------------------------------- /Sources/SwiftGitX/SwiftGitX.docc/SwiftGitX.md: -------------------------------------------------------------------------------- 1 | # ``SwiftGitX`` 2 | 3 | @Metadata { 4 | @PageImage(purpose: icon, source: "Git-Icon-White") 5 | } 6 | 7 | Modern Swift bindings to libgit2. 8 | 9 | ## Overview 10 | 11 | SwiftGitX provides modern and easy to use bindings to [libgit2](https://libgit2.org). If you familiar with git terminal cli, learning SwiftGitX is easy. 12 | 13 | ## Topics 14 | 15 | ### Essentials 16 | 17 | - 18 | - ``SwiftGitX`` 19 | - ``Repository`` 20 | 21 | ### Objects 22 | 23 | - 24 | 25 | ### References 26 | 27 | - 28 | 29 | ### Collections 30 | 31 | - 32 | 33 | ### Models 34 | 35 | - 36 | - 37 | 38 | ### Iterators 39 | 40 | - 41 | 42 | ### Error Types 43 | 44 | - 45 | -------------------------------------------------------------------------------- /Sources/SwiftGitX/SwiftGitX.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | 3 | public enum SwiftGitXError: Error { 4 | case failedToInitialize(String) 5 | case failedToShutdown(String) 6 | } 7 | 8 | /// The main entry point for the SwiftGitX library. 9 | public enum SwiftGitX { 10 | /// Initialize the SwiftGitX 11 | /// 12 | /// - Returns: the number of initializations of the library. 13 | /// 14 | /// This function must be called before any other libgit2 function in order to set up global state and threading. 15 | /// 16 | /// This function may be called multiple times. It will return the number of times the initialization has been 17 | /// called (including this one) that have not subsequently been shutdown. 18 | @discardableResult 19 | public static func initialize() throws -> Int { 20 | // Initialize the libgit2 library 21 | let status = git_libgit2_init() 22 | 23 | guard status >= 0 else { 24 | let errorMessage = String(cString: git_error_last().pointee.message) 25 | throw SwiftGitXError.failedToInitialize(errorMessage) 26 | } 27 | 28 | return Int(status) 29 | } 30 | 31 | /// Shutdown the SwiftGitX 32 | /// 33 | /// - Returns: the number of shutdowns of the library. 34 | /// 35 | /// Clean up the global state and threading context after calling it as many times as ``initialize()`` was called. 36 | /// It will return the number of remaining initializations that have not been shutdown (after this one). 37 | @discardableResult 38 | public static func shutdown() throws -> Int { 39 | // Shutdown the libgit2 library 40 | let status = git_libgit2_shutdown() 41 | 42 | guard status >= 0 else { 43 | let errorMessage = String(cString: git_error_last().pointee.message) 44 | throw SwiftGitXError.failedToShutdown(errorMessage) 45 | } 46 | 47 | return Int(status) 48 | } 49 | 50 | /// The version of the libgit2 library. 51 | public static var libgit2Version: String { 52 | var major: Int32 = 0 53 | var minor: Int32 = 0 54 | var patch: Int32 = 0 55 | 56 | git_libgit2_version(&major, &minor, &patch) 57 | 58 | return "\(major).\(minor).\(patch)" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/SwiftGitX.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "B08F7335-0FFC-46A5-AC8B-B4FF20AF033D", 5 | "name" : "Test Scheme Action", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | "testRepetitionMode" : "retryOnFailure" 13 | }, 14 | "testTargets" : [ 15 | { 16 | "skippedTests" : [ 17 | "SwiftGitXTestCase" 18 | ], 19 | "target" : { 20 | "containerPath" : "container:", 21 | "identifier" : "SwiftGitXTests", 22 | "name" : "SwiftGitXTests" 23 | } 24 | } 25 | ], 26 | "version" : 1 27 | } 28 | -------------------------------------------------------------------------------- /Tests/SwiftGitXTests/CollectionTests/ConfigCollection.swift: -------------------------------------------------------------------------------- 1 | import SwiftGitX 2 | import XCTest 3 | 4 | final class ConfigCollectionTests: SwiftGitXTestCase { 5 | func testConfigDefaultBranchName() { 6 | let repository = Repository.mock(named: "test-config-default-branch-name", in: Self.directory) 7 | 8 | // Set local default branch name 9 | repository.config.set("feature", forKey: "init.defaultBranch") 10 | 11 | XCTAssertEqual(repository.config.defaultBranchName, "feature") 12 | } 13 | 14 | func testConfigSet() { 15 | let repository = Repository.mock(named: "test-config-set", in: Self.directory) 16 | 17 | // Set local default branch name 18 | repository.config.set("develop", forKey: "init.defaultBranch") 19 | 20 | // Test if the default branch name is set 21 | XCTAssertEqual(repository.config.defaultBranchName, "develop") 22 | // Global default branch name should not be changed 23 | XCTAssertEqual(Repository.config.defaultBranchName, "main") 24 | } 25 | 26 | func testConfigString() { 27 | let repository = Repository.mock(named: "test-config-string", in: Self.directory) 28 | 29 | // Set local user name and email 30 | repository.config.set("İbrahim Çetin", forKey: "user.name") 31 | repository.config.set("mail@ibrahimcetin.dev", forKey: "user.email") 32 | 33 | XCTAssertEqual(repository.config.string(forKey: "user.name"), "İbrahim Çetin") 34 | XCTAssertEqual(repository.config.string(forKey: "user.email"), "mail@ibrahimcetin.dev") 35 | } 36 | 37 | func testConfigGlobalString() { 38 | // Get global default branch name 39 | XCTAssertEqual(Repository.config.string(forKey: "init.defaultBranch"), "main") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/SwiftGitXTests/CollectionTests/IndexCollectionTests.swift: -------------------------------------------------------------------------------- 1 | @testable import SwiftGitX 2 | import XCTest 3 | 4 | final class IndexCollectionTests: SwiftGitXTestCase { 5 | func testIndexAddPath() throws { 6 | // Create a repository 7 | let repository = Repository.mock(named: "test-index-add-path", in: Self.directory) 8 | 9 | // Create a file in the repository 10 | _ = try repository.mockFile(named: "README.md", content: "Hello, World!") 11 | 12 | // Stage the file using the file path 13 | XCTAssertNoThrow(try repository.add(path: "README.md")) 14 | 15 | // Verify that the file is staged 16 | let statusEntry = try XCTUnwrap(repository.status().first) 17 | 18 | XCTAssertEqual(statusEntry.status, [.indexNew]) // The file is staged 19 | XCTAssertEqual(statusEntry.index?.newFile.path, "README.md") 20 | XCTAssertNil(statusEntry.workingTree) // The file is staged and not in the working tree anymore 21 | } 22 | 23 | func testIndexAddFile() throws { 24 | // Create a repository 25 | let repository = Repository.mock(named: "test-index-add-file", in: Self.directory) 26 | 27 | // Create a file in the repository 28 | let file = try repository.mockFile(named: "README.md", content: "Hello, World!") 29 | 30 | // Stage the file using the file URL 31 | XCTAssertNoThrow(try repository.add(file: file)) 32 | 33 | // Verify that the file is staged 34 | let statusEntry = try XCTUnwrap(repository.status().first) 35 | 36 | XCTAssertEqual(statusEntry.status, [.indexNew]) // The file is staged 37 | XCTAssertEqual(statusEntry.index?.newFile.path, "README.md") 38 | XCTAssertNil(statusEntry.workingTree) // The file is staged and not in the working tree anymore 39 | } 40 | 41 | func testIndexAddPaths() throws { 42 | // Create a repository 43 | let repository = Repository.mock(named: "test-index-add-paths", in: Self.directory) 44 | 45 | // Create new files in the repository 46 | let files = try (0 ..< 10).map { index in 47 | try repository.mockFile(named: "README-\(index).md", content: "Hello, World!") 48 | } 49 | 50 | // Stage the files using the file paths 51 | XCTAssertNoThrow(try repository.add(paths: files.map(\.lastPathComponent))) 52 | 53 | // Verify that the files are staged 54 | let statusEntries = try repository.status() 55 | 56 | XCTAssertEqual(statusEntries.count, files.count) 57 | XCTAssertEqual(statusEntries.map(\.status), Array(repeating: [.indexNew], count: files.count)) 58 | XCTAssertEqual(statusEntries.map(\.index?.newFile.path), files.map(\.lastPathComponent)) 59 | XCTAssertEqual(statusEntries.map(\.workingTree), Array(repeating: nil, count: files.count)) 60 | } 61 | 62 | func testIndexAddFiles() throws { 63 | // Create a repository 64 | let repository = Repository.mock(named: "test-index-add-files", in: Self.directory) 65 | 66 | // Create new files in the repository 67 | let files = try (0 ..< 10).map { index in 68 | try repository.mockFile(named: "README-\(index).md", content: "Hello, World!") 69 | } 70 | 71 | // Stage the files using the file URLs 72 | XCTAssertNoThrow(try repository.add(files: files)) 73 | 74 | // Verify that the files are staged 75 | let statusEntries = try repository.status() 76 | 77 | XCTAssertEqual(statusEntries.count, files.count) 78 | XCTAssertEqual(statusEntries.map(\.status), Array(repeating: [.indexNew], count: files.count)) 79 | XCTAssertEqual(statusEntries.map(\.index?.newFile.path), files.map(\.lastPathComponent)) 80 | XCTAssertEqual(statusEntries.map(\.workingTree), Array(repeating: nil, count: files.count)) 81 | } 82 | 83 | // TODO: Add test for add all 84 | 85 | func testIndexRemovePath() throws { 86 | // Create a repository 87 | let repository = Repository.mock(named: "test-index-remove-path", in: Self.directory) 88 | 89 | // Create a file in the repository 90 | let file = try repository.mockFile(named: "README.md", content: "Hello, World!") 91 | 92 | // Stage the file 93 | XCTAssertNoThrow(try repository.add(file: file)) 94 | 95 | // Unstage the file using the file path 96 | XCTAssertNoThrow(try repository.remove(path: "README.md")) 97 | 98 | // Verify that the file is not staged 99 | let statusEntry = try XCTUnwrap(repository.status().first) 100 | 101 | XCTAssertEqual(statusEntry.status, [.workingTreeNew]) 102 | XCTAssertNil(statusEntry.index) // The file is not staged 103 | } 104 | 105 | func testIndexRemoveFile() throws { 106 | // Create a repository 107 | let repository = Repository.mock(named: "test-index-remove-file", in: Self.directory) 108 | 109 | // Create a file in the repository 110 | let file = try repository.mockFile(named: "README.md", content: "Hello, World!") 111 | 112 | // Stage the file 113 | XCTAssertNoThrow(try repository.add(file: file)) 114 | 115 | // Unstage the file using the file URL 116 | XCTAssertNoThrow(try repository.remove(file: file)) 117 | 118 | // Verify that the file is not staged 119 | let statusEntry = try XCTUnwrap(repository.status().first) 120 | 121 | XCTAssertEqual(statusEntry.status, [.workingTreeNew]) 122 | XCTAssertNil(statusEntry.index) // The file is not staged 123 | } 124 | 125 | func testIndexRemovePaths() throws { 126 | // Create a repository 127 | let repository = Repository.mock(named: "test-index-remove-paths", in: Self.directory) 128 | 129 | // Create new files in the repository 130 | let files = try (0 ..< 10).map { index in 131 | try repository.mockFile(named: "README-\(index).md", content: "Hello, World!") 132 | } 133 | 134 | // Stage the files 135 | XCTAssertNoThrow(try repository.add(files: files)) 136 | 137 | // Unstage the files using the file paths 138 | XCTAssertNoThrow(try repository.remove(paths: files.map(\.lastPathComponent))) 139 | 140 | // Verify that the files are not staged 141 | let statusEntries = try repository.status() 142 | 143 | XCTAssertEqual(statusEntries.count, files.count) 144 | XCTAssertEqual(statusEntries.map(\.status), Array(repeating: [.workingTreeNew], count: files.count)) 145 | XCTAssertEqual(statusEntries.map(\.index), Array(repeating: nil, count: files.count)) 146 | } 147 | 148 | func testIndexRemoveFiles() throws { 149 | // Create a repository 150 | let repository = Repository.mock(named: "test-index-remove-files", in: Self.directory) 151 | 152 | // Create new files in the repository 153 | let files = try (0 ..< 10).map { index in 154 | try repository.mockFile(named: "README-\(index).md", content: "Hello, World!") 155 | } 156 | 157 | // Stage the files 158 | XCTAssertNoThrow(try repository.add(files: files)) 159 | 160 | // Unstage the files using the file URLs 161 | XCTAssertNoThrow(try repository.remove(files: files)) 162 | 163 | // Verify that the files are not staged 164 | let statusEntries = try repository.status() 165 | 166 | XCTAssertEqual(statusEntries.count, files.count) 167 | XCTAssertEqual(statusEntries.map(\.status), Array(repeating: [.workingTreeNew], count: files.count)) 168 | XCTAssertEqual(statusEntries.map(\.index), Array(repeating: nil, count: files.count)) 169 | } 170 | 171 | func testIndexRemoveAll() throws { 172 | // Create a repository 173 | let repository = Repository.mock(named: "test-index-remove-all", in: Self.directory) 174 | 175 | // Create new files in the repository 176 | let files = try (0 ..< 10).map { index in 177 | try repository.mockFile(named: "README-\(index).md", content: "Hello, World!") 178 | } 179 | 180 | // Stage the files 181 | XCTAssertNoThrow(try repository.add(files: files)) 182 | 183 | // Unstage all files 184 | XCTAssertNoThrow(try repository.index.removeAll()) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /Tests/SwiftGitXTests/CollectionTests/ReferenceCollectionTests.swift: -------------------------------------------------------------------------------- 1 | import SwiftGitX 2 | import XCTest 3 | 4 | final class ReferenceCollectionTests: SwiftGitXTestCase { 5 | func testReferenceLookupSubscript() throws { 6 | // Create a new repository at the temporary directory 7 | let repository = Repository.mock(named: "test-reference-lookup-subscript", in: Self.directory) 8 | 9 | // Create mock commit 10 | let commit = try repository.mockCommit() 11 | 12 | // Get the branch 13 | guard let reference = repository.reference["refs/heads/main"] else { 14 | XCTFail("Reference not found") 15 | return 16 | } 17 | 18 | // Check the reference 19 | XCTAssertEqual(reference.name, "main") 20 | XCTAssertEqual(reference.fullName, "refs/heads/main") 21 | XCTAssertEqual(reference.target.id, commit.id) 22 | } 23 | 24 | func testReferenceLookupSubscriptFailure() throws { 25 | // Create a new repository at the temporary directory 26 | let repository = Repository.mock(named: "test-reference-lookup-subscript-failure", in: Self.directory) 27 | 28 | // Create mock commit 29 | try repository.mockCommit() 30 | 31 | // Get the branch 32 | let reference = repository.reference["refs/heads/feature"] 33 | 34 | // Check the reference 35 | XCTAssertNil(reference) 36 | } 37 | 38 | func testReferenceLookupBranch() throws { 39 | // Create a new repository at the temporary directory 40 | let repository = Repository.mock(named: "test-reference-lookup-branch", in: Self.directory) 41 | 42 | // Create mock commit 43 | let commit = try repository.mockCommit() 44 | 45 | // Create a new branch 46 | let branch = try repository.branch.create(named: "feature", target: commit) 47 | 48 | // Get the branch 49 | let reference = try repository.reference.get(named: branch.fullName) 50 | 51 | // Check the reference 52 | XCTAssertEqual(reference.name, branch.name) 53 | XCTAssertEqual(reference.fullName, branch.fullName) 54 | XCTAssertEqual(reference.target.id, commit.id) 55 | } 56 | 57 | func testReferenceLookupTagAnnotated() throws { 58 | // Create a new repository at the temporary directory 59 | let repository = Repository.mock(named: "test-reference-lookup-tag-annotated", in: Self.directory) 60 | 61 | // Create mock commit 62 | let commit = try repository.mockCommit() 63 | 64 | // Create a new tag 65 | let tag = try repository.tag.create(named: "v1.0.0", target: commit) 66 | 67 | // Get the tag 68 | let reference = try repository.reference.get(named: tag.fullName) 69 | 70 | // Check the reference 71 | XCTAssertEqual(reference.name, "v1.0.0") 72 | XCTAssertEqual(reference.fullName, "refs/tags/v1.0.0") 73 | XCTAssertEqual(reference.target.id, commit.id) 74 | } 75 | 76 | func testReferenceLookupTagLightweight() throws { 77 | // Create a new repository at the temporary directory 78 | let repository = Repository.mock(named: "test-reference-lookup-tag-lightweight", in: Self.directory) 79 | 80 | // Create mock commit 81 | let commit = try repository.mockCommit() 82 | 83 | // Create a new tag 84 | let tag = try repository.tag.create(named: "v1.0.0", target: commit, type: .lightweight) 85 | 86 | // Get the tag 87 | let reference = try repository.reference.get(named: tag.fullName) 88 | 89 | // Check the reference 90 | XCTAssertEqual(reference.name, "v1.0.0") 91 | XCTAssertEqual(reference.fullName, "refs/tags/v1.0.0") 92 | XCTAssertEqual(reference.target.id, commit.id) 93 | } 94 | 95 | func testReferenceLookupFailure() throws { 96 | // Create a new repository at the temporary directory 97 | let repository = Repository.mock(named: "test-reference-lookup-failure", in: Self.directory) 98 | 99 | // Create mock commit 100 | try repository.mockCommit() 101 | 102 | // Get the branch 103 | XCTAssertThrowsError(try repository.reference.get(named: "refs/heads/feature")) { error in 104 | XCTAssertEqual(error as? ReferenceError, .notFound) 105 | } 106 | } 107 | 108 | func testReferenceList() throws { 109 | // Create a new repository at the temporary directory 110 | let repository = Repository.mock(named: "test-reference-list", in: Self.directory) 111 | 112 | // Create mock commit 113 | let commit = try repository.mockCommit() 114 | 115 | // Create a new branch 116 | try repository.branch.create(named: "feature", target: commit) 117 | 118 | // Create a new tag 119 | try repository.tag.create(named: "v1.0.0", target: commit) 120 | 121 | // Get the references 122 | let references = try repository.reference.list() 123 | 124 | // Check the reference 125 | XCTAssertEqual(references.count, 3) 126 | 127 | let referenceNames = references.map(\.name) 128 | XCTAssertTrue(referenceNames.contains("feature")) 129 | XCTAssertTrue(referenceNames.contains("main")) 130 | XCTAssertTrue(referenceNames.contains("v1.0.0")) 131 | } 132 | 133 | func testReferenceIterator() throws { 134 | // Create a new repository at the temporary directory 135 | let repository = Repository.mock(named: "test-reference-iterator", in: Self.directory) 136 | 137 | // Create mock commit 138 | let commit = try repository.mockCommit() 139 | 140 | // Create a new branch 141 | try repository.branch.create(named: "feature", target: commit) 142 | 143 | // Create a new tag 144 | try repository.tag.create(named: "v1.0.0", target: commit) 145 | 146 | // Get the references from iterator 147 | let references = Array(repository.reference) 148 | 149 | // Check the reference 150 | XCTAssertEqual(references.count, 3) 151 | 152 | let referenceNames = references.map(\.name) 153 | XCTAssertTrue(referenceNames.contains("feature")) 154 | XCTAssertTrue(referenceNames.contains("main")) 155 | XCTAssertTrue(referenceNames.contains("v1.0.0")) 156 | } 157 | 158 | func testReferenceIteratorGlob() throws { 159 | // Create a new repository at the temporary directory 160 | let repository = Repository.mock(named: "test-reference-iterator-glob", in: Self.directory) 161 | 162 | // Create mock commit 163 | let commit = try repository.mockCommit() 164 | 165 | // Create a new tag 166 | let tag = try repository.tag.create(named: "v1.0.0", target: commit) 167 | 168 | // Get the references from iterator 169 | let references = try repository.reference.list(glob: "refs/tags/*") 170 | 171 | // Check the references 172 | XCTAssertEqual(references.count, 1) 173 | let tagLookup = try XCTUnwrap(references.first as? Tag) 174 | 175 | XCTAssertEqual(tagLookup, tag) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /Tests/SwiftGitXTests/CollectionTests/RemoteCollectionTests.swift: -------------------------------------------------------------------------------- 1 | import SwiftGitX 2 | import XCTest 3 | 4 | final class RemoteCollectionTests: SwiftGitXTestCase { 5 | func testRemoteLookup() throws { 6 | // Create a mock repository at the temporary directory 7 | let repository = Repository.mock(named: "test-remote-lookup", in: Self.directory) 8 | 9 | // Add a remote to the repository 10 | let url = URL(string: "https://github.com/username/repo.git")! 11 | let remote = try repository.remote.add(named: "origin", at: url) 12 | 13 | // Get the remote from the repository 14 | let remoteLookup = try repository.remote.get(named: "origin") 15 | 16 | // Check if the remote is the same 17 | XCTAssertEqual(remoteLookup, remote) 18 | 19 | XCTAssertEqual(remote.name, "origin") 20 | XCTAssertEqual(remote.url, url) 21 | } 22 | 23 | func testRemoteAdd() throws { 24 | // Create a mock repository at the temporary directory 25 | let repository = Repository.mock(named: "test-remote-add", in: Self.directory) 26 | 27 | // Add a new remote to the repository 28 | let url = URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! 29 | let remote = try repository.remote.add(named: "origin", at: url) 30 | 31 | // Get the remote from the repository 32 | let remoteLookup = try repository.remote.get(named: "origin") 33 | 34 | // Check if the remote is the same 35 | XCTAssertEqual(remoteLookup, remote) 36 | 37 | XCTAssertEqual(remote.name, "origin") 38 | XCTAssertEqual(remote.url, url) 39 | } 40 | 41 | func testRemoteBranches() async throws { 42 | // Create a mock repository at the temporary directory 43 | let remoteRepository = Repository.mock(named: "test-remote-branches--remote", in: Self.directory) 44 | 45 | // Create a commit in the repository 46 | try remoteRepository.mockCommit() 47 | 48 | // Create branches in the repository 49 | try ["feature/1", "feature/2", "feature/3", "feature/4", "feature/5", "feature/6", "feature/7"] 50 | .forEach { name in 51 | try remoteRepository.branch.create(named: name, from: remoteRepository.branch.current) 52 | } 53 | let branches = Array(remoteRepository.branch.local) 54 | 55 | XCTAssertEqual(branches.count, 8) 56 | 57 | // Clone remote repository to local repository 58 | let localDirectory = Repository.mockDirectory(named: "test-remote-branches--local", in: Self.directory) 59 | let localRepository = try await Repository.clone(from: remoteRepository.workingDirectory, to: localDirectory) 60 | 61 | // Get the remote from the repository excluding the main branch 62 | let remoteBranches = Array(localRepository.branch.remote) 63 | 64 | // Check if the branches are the same 65 | XCTAssertEqual(remoteBranches.count, 8) 66 | 67 | for (remoteBranch, branch) in zip(remoteBranches, branches) { 68 | XCTAssertEqual(remoteBranch.name, "origin/" + branch.name) 69 | } 70 | } 71 | 72 | func testRemoteRemove() throws { 73 | // Create a mock repository at the temporary directory 74 | let repository = Repository.mock(named: "test-remote-remove", in: Self.directory) 75 | 76 | // Add a remote to the repository 77 | let remote = try repository.remote.add( 78 | named: "origin", 79 | at: URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! 80 | ) 81 | 82 | // Remove the remote from the repository 83 | try repository.remote.remove(remote) 84 | 85 | // Get the remote from the repository 86 | XCTAssertThrowsError(try repository.remote.get(named: "origin")) { error in 87 | XCTAssertEqual(error as? RemoteError, .notFound("remote \'origin\' does not exist")) 88 | } 89 | } 90 | 91 | func testRemoteList() throws { 92 | // Create a mock repository at the temporary directory 93 | let repository = Repository.mock(named: "test-remote-list", in: Self.directory) 94 | 95 | // Add remotes to the repository 96 | let remoteNames = ["origin", "upstream", "features", "my-remote", "remote"] 97 | let remotes = try remoteNames.map { name in 98 | try repository.remote.add(named: name, at: URL(string: "https://example.com/\(name).git")!) 99 | } 100 | 101 | // List the remotes in the repository 102 | let remoteLookups = try repository.remote.list() 103 | 104 | XCTAssertEqual(Set(remotes), Set(remoteLookups)) 105 | } 106 | 107 | func testRemoteIterator() throws { 108 | // Create a mock repository at the temporary directory 109 | let repository = Repository.mock(named: "test-remote-iterator", in: Self.directory) 110 | 111 | // Add remotes to the repository 112 | let remoteNames = ["origin", "upstream", "features", "my-remote", "remote"] 113 | let remotes = try remoteNames.map { name in 114 | try repository.remote.add(named: name, at: URL(string: "https://example.com/\(name).git")!) 115 | } 116 | 117 | // List the remotes in the repository 118 | let remoteLookups = Array(repository.remote) 119 | 120 | XCTAssertEqual(Set(remotes), Set(remoteLookups)) 121 | } 122 | 123 | func testRemoteLookupNotFound() throws { 124 | // Create a new repository at the temporary directory 125 | let repository = Repository.mock(named: "test-remote-not-found", in: Self.directory) 126 | 127 | // Get the remote 128 | XCTAssertThrowsError(try repository.remote.get(named: "origin")) { error in 129 | XCTAssertEqual(error as? RemoteError, .notFound("remote \'origin\' does not exist")) 130 | } 131 | } 132 | 133 | func testRemoteAddFailure() throws { 134 | // Create a mock repository at the temporary directory 135 | let repository = Repository.mock(named: "test-remote-remove", in: Self.directory) 136 | 137 | // Add a remote to the repository 138 | let remote = try repository.remote.add( 139 | named: "origin", 140 | at: URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! 141 | ) 142 | 143 | // Add the same remote again 144 | XCTAssertThrowsError(try repository.remote.add(named: "origin", at: remote.url)) { error in 145 | XCTAssertEqual(error as? RemoteCollectionError, .remoteAlreadyExists("remote \'origin\' already exists")) 146 | } 147 | } 148 | 149 | func testRemoteRemoveFailure() throws { 150 | // Create a mock repository at the temporary directory 151 | let repository = Repository.mock(named: "test-remote-remove", in: Self.directory) 152 | 153 | // Add a remote to the repository 154 | let remote = try repository.remote.add( 155 | named: "origin", 156 | at: URL(string: "https://github.com/ibrahimcetin/SwiftGitX.git")! 157 | ) 158 | 159 | // Remove the remote from the repository 160 | try repository.remote.remove(remote) 161 | 162 | // Remove the remote again 163 | XCTAssertThrowsError(try repository.remote.remove(remote)) { error in 164 | XCTAssertEqual(error as? RemoteCollectionError, .failedToRemove("remote \'origin\' does not exist")) 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Tests/SwiftGitXTests/CollectionTests/StashCollectionTests.swift: -------------------------------------------------------------------------------- 1 | import SwiftGitX 2 | import XCTest 3 | 4 | final class StashCollectionTests: SwiftGitXTestCase { 5 | func testStashSave() throws { 6 | // Create a new repository at the temporary directory 7 | let repository = Repository.mock(named: "test-stash-save", in: Self.directory) 8 | 9 | // Create mock commit 10 | try repository.mockCommit() 11 | 12 | // Create a file 13 | let fileURL = try URL(fileURLWithPath: "test.txt", relativeTo: repository.workingDirectory) 14 | FileManager.default.createFile(atPath: fileURL.path, contents: Data("Stash me!".utf8)) 15 | 16 | // Stage the file 17 | try repository.add(path: fileURL.lastPathComponent) 18 | 19 | // Create a new stash entry 20 | try repository.stash.save() 21 | 22 | // List the stash entries 23 | let stashes = try repository.stash.list() 24 | 25 | // Check the stash entries 26 | XCTAssertEqual(stashes.count, 1) 27 | } 28 | 29 | func testStashSaveFailure() throws { 30 | // Create a new repository at the temporary directory 31 | let repository = Repository.mock(named: "test-stash-save-failure", in: Self.directory) 32 | 33 | // Create mock commit 34 | try repository.mockCommit() 35 | 36 | // Create a new stash entry 37 | XCTAssertThrowsError(try repository.stash.save()) { error in 38 | XCTAssertEqual(error as? StashCollectionError, .noLocalChangesToSave) 39 | } 40 | } 41 | 42 | func testStashList() throws { 43 | // Create a new repository at the temporary directory 44 | let repository = Repository.mock(named: "test-stash-list", in: Self.directory) 45 | 46 | // Create mock commit 47 | try repository.mockCommit() 48 | 49 | for index in 0 ..< 5 { 50 | // Create a file 51 | _ = try repository.mockFile(named: "test\(index).txt", content: "Stash me!") 52 | 53 | // Create a new stash 54 | try repository.stash.save(message: "Stashed \(index)!", options: .includeUntracked) 55 | } 56 | 57 | // List the stash entries 58 | let stashes = try repository.stash.list() 59 | 60 | // Check the stash entries 61 | XCTAssertEqual(stashes.count, 5) 62 | } 63 | 64 | func testStashIterator() throws { 65 | // Create a new repository at the temporary directory 66 | let repository = Repository.mock(named: "test-stash-iterator", in: Self.directory) 67 | 68 | // Create mock commit 69 | try repository.mockCommit() 70 | 71 | for index in 0 ..< 5 { 72 | // Create a file 73 | _ = try repository.mockFile(named: "test-\(index).txt", content: "Stash me!") 74 | 75 | // Create a new stash 76 | try repository.stash.save(message: "Stashed \(index)!", options: .includeUntracked) 77 | } 78 | 79 | // Iterate over the stash entries 80 | for (index, entry) in repository.stash.enumerated() { 81 | XCTAssertEqual(entry.index, index) 82 | XCTAssertEqual(entry.message, "On main: Stashed \(4 - index)!") 83 | } 84 | } 85 | 86 | func testStashApply() throws { 87 | // Create a new repository at the temporary directory 88 | let repository = Repository.mock(named: "test-stash-apply", in: Self.directory) 89 | 90 | // Create mock commit 91 | try repository.mockCommit() 92 | 93 | // Create a file 94 | let fileURL = try URL(fileURLWithPath: "test.txt", relativeTo: repository.workingDirectory) 95 | FileManager.default.createFile(atPath: fileURL.path, contents: Data("Stash me!".utf8)) 96 | 97 | // Create a new stash entry 98 | try repository.stash.save(options: .includeUntracked) 99 | 100 | XCTAssertEqual(try repository.stash.list().count, 1) 101 | XCTAssertFalse(FileManager.default.fileExists(atPath: fileURL.path)) 102 | 103 | // Apply the stash entry 104 | try repository.stash.apply() 105 | 106 | // List the stashes 107 | let stashes = try repository.stash.list() 108 | 109 | // Check the stash entries 110 | XCTAssertEqual(stashes.count, 1) // The stash should still exist 111 | XCTAssertTrue(FileManager.default.fileExists(atPath: fileURL.path)) 112 | XCTAssertEqual(try String(contentsOf: fileURL), "Stash me!") 113 | } 114 | 115 | func testStashPop() throws { 116 | // Create a new repository at the temporary directory 117 | let repository = Repository.mock(named: "test-stash-pop", in: Self.directory) 118 | 119 | // Create mock commit 120 | try repository.mockCommit() 121 | 122 | // Create a file 123 | let fileURL = try URL(fileURLWithPath: "test.txt", relativeTo: repository.workingDirectory) 124 | FileManager.default.createFile(atPath: fileURL.path, contents: Data("Stash me!".utf8)) 125 | 126 | // Create a new stash entry 127 | try repository.stash.save(options: .includeUntracked) 128 | 129 | XCTAssertEqual(try repository.stash.list().count, 1) 130 | XCTAssertFalse(FileManager.default.fileExists(atPath: fileURL.path)) 131 | 132 | // Apply the stash entry 133 | try repository.stash.pop() 134 | 135 | // List the stashes 136 | let stashes = try repository.stash.list() 137 | 138 | // Check the stash entries 139 | XCTAssertEqual(stashes.count, 0) // The stash should be removed 140 | XCTAssertTrue(FileManager.default.fileExists(atPath: fileURL.path)) 141 | XCTAssertEqual(try String(contentsOf: fileURL), "Stash me!") 142 | } 143 | 144 | func testStashDrop() throws { 145 | // Create a new repository at the temporary directory 146 | let repository = Repository.mock(named: "test-stash-drop", in: Self.directory) 147 | 148 | // Create mock commit 149 | try repository.mockCommit() 150 | 151 | // Create a file 152 | let fileURL = try URL(fileURLWithPath: "test.txt", relativeTo: repository.workingDirectory) 153 | FileManager.default.createFile(atPath: fileURL.path, contents: Data("Stash me!".utf8)) 154 | 155 | // Create a new stash entry 156 | try repository.stash.save(options: .includeUntracked) 157 | 158 | // Drop the stash entry 159 | try repository.stash.drop() 160 | 161 | // List the stash entries 162 | let stashes = try repository.stash.list() 163 | 164 | // Check the stash entries 165 | XCTAssertEqual(stashes.count, 0) 166 | XCTAssertFalse(FileManager.default.fileExists(atPath: fileURL.path)) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Tests/SwiftGitXTests/ObjectTests.swift: -------------------------------------------------------------------------------- 1 | import libgit2 2 | @testable import SwiftGitX 3 | import XCTest 4 | 5 | final class ObjectTests: SwiftGitXTestCase { 6 | func testOID() throws { 7 | // Test OID hex initialization 8 | let shaHex = "42a02b346bb0fb0db7eff3cffeb3c70babbd2045" 9 | let oid = try OID(hex: shaHex) 10 | 11 | // Check if the OID hex is correct 12 | XCTAssertEqual(oid.hex, shaHex) 13 | 14 | // Check if the OID abbreviated is correct 15 | let abbreviatedSHA = "42a02b34" 16 | XCTAssertEqual(oid.abbreviated, abbreviatedSHA) 17 | 18 | // Check if the OID raw is correct 19 | var rawOID = git_oid() 20 | git_oid_fromstr(&rawOID, shaHex) 21 | 22 | XCTAssertEqual(oid, OID(raw: rawOID)) 23 | 24 | // Test OID is zero 25 | let zeroOID = OID.zero 26 | 27 | XCTAssertEqual(zeroOID.hex, "0000000000000000000000000000000000000000") 28 | XCTAssertEqual(zeroOID.abbreviated, "00000000") 29 | 30 | var zeroOIDRaw = zeroOID.raw 31 | XCTAssertEqual(git_oid_is_zero(&zeroOIDRaw), 1) 32 | 33 | XCTAssertEqual(zeroOID, .zero) 34 | } 35 | 36 | func testCommit() throws { 37 | // Create mock repository at the temporary directory 38 | let repository = Repository.mock(named: "test-object-commit", in: Self.directory) 39 | 40 | // Create a new file in the repository 41 | let file = try repository.workingDirectory.appending(component: "README.md") 42 | FileManager.default.createFile(atPath: file.path, contents: nil) 43 | 44 | // Add the file to the index 45 | XCTAssertNoThrow(try repository.add(file: file)) 46 | 47 | // Commit the changes 48 | let initialCommit = try repository.commit(message: "Initial commit") 49 | 50 | // TODO: Get default signature 51 | 52 | XCTAssertEqual(initialCommit.id, try repository.HEAD.target.id) 53 | XCTAssertEqual(initialCommit.message, "Initial commit") 54 | 55 | // Check if the commit has no parent 56 | XCTAssertEqual(try initialCommit.parents.count, 0) 57 | 58 | // Add content to the file 59 | try Data("Hello, World!".utf8).write(to: file) 60 | 61 | // Add the file to the index 62 | XCTAssertNoThrow(try repository.add(path: "README.md")) 63 | 64 | // Commit the changes 65 | let commit = try repository.commit(message: "Add content to README.md") 66 | 67 | // Check if the commit has the correct parent 68 | XCTAssertEqual(try commit.parents.count, 1) 69 | 70 | let parentCommit: Commit = try repository.show(id: commit.parents.first!.id) 71 | XCTAssertEqual(parentCommit, initialCommit) 72 | } 73 | 74 | func testTagAnnotated() throws { 75 | // Create a new repository at the temporary directory 76 | let repository = Repository.mock(named: "test-object-tag-annotated", in: Self.directory) 77 | 78 | // Commit the changes 79 | let commit = try repository.mockCommit() 80 | 81 | // Create a new tag 82 | let tag = try repository.tag.create( 83 | named: "v1.0.0", target: commit, message: "Initial release" 84 | ) 85 | 86 | // Check if the tag is the same 87 | XCTAssertEqual(tag.name, "v1.0.0") 88 | XCTAssertEqual(tag.fullName, "refs/tags/v1.0.0") 89 | 90 | XCTAssertEqual(tag.target.id, commit.id) 91 | XCTAssertEqual(tag.message, "Initial release") 92 | 93 | // TODO: Check tagger signature 94 | } 95 | 96 | func testTagLightweight() throws { 97 | // Create a new repository at the temporary directory 98 | let repository = Repository.mock(named: "test-object-tag-lightweight", in: Self.directory) 99 | 100 | // Commit the changes 101 | let commit = try repository.mockCommit() 102 | 103 | // Create a new tag 104 | let tag = try repository.tag.create(named: "v1.0.0", target: commit, type: .lightweight) 105 | 106 | // Check the tag properties 107 | XCTAssertEqual(tag.name, "v1.0.0") 108 | XCTAssertEqual(tag.fullName, "refs/tags/v1.0.0") 109 | 110 | XCTAssertEqual(tag.id, commit.id) 111 | XCTAssertEqual(tag.id, tag.target.id) 112 | XCTAssertEqual(tag.target.id, commit.id) 113 | 114 | XCTAssertNil(tag.tagger) 115 | XCTAssertNil(tag.message) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Tests/SwiftGitXTests/PerformanceTests/RepositoryPerformanceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepositoryPerformanceTests.swift 3 | // 4 | // 5 | // Created by İbrahim Çetin on 21.04.2024. 6 | // 7 | 8 | import SwiftGitX 9 | import XCTest 10 | 11 | final class RepositoryPerformanceTests: SwiftGitXTestCase { 12 | private let options: XCTMeasureOptions = { 13 | let options = XCTMeasureOptions.default 14 | 15 | options.invocationOptions = [.manuallyStart, .manuallyStop] 16 | options.iterationCount = 10 17 | 18 | return options 19 | }() 20 | 21 | func testPerformanceAdd() throws { 22 | // Create a repository 23 | let repository = Repository.mock(named: "test-performance-add", in: Self.directory) 24 | 25 | measure(options: options) { 26 | do { 27 | let file = try repository.mockFile(named: UUID().uuidString) 28 | 29 | // Measure the time it takes to add a file 30 | startMeasuring() 31 | try repository.add(file: file) 32 | stopMeasuring() 33 | } catch { 34 | XCTFail(error.localizedDescription) 35 | } 36 | } 37 | } 38 | 39 | func testPerformanceCommit() throws { 40 | // Create a repository 41 | let repository = Repository.mock(named: "test-performance-commit", in: Self.directory) 42 | 43 | measure(options: options) { 44 | do { 45 | // Add a file to the index 46 | try repository.add(file: repository.mockFile(named: UUID().uuidString)) 47 | 48 | // Measure the time it takes to commit the file 49 | startMeasuring() 50 | try repository.commit(message: "Commit message") 51 | stopMeasuring() 52 | } catch { 53 | XCTFail(error.localizedDescription) 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/SwiftGitXTests/RepositoryTests/RepositoryOperationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepositoryOperationTests.swift 3 | // 4 | // 5 | // Created by İbrahim Çetin on 18.06.2024. 6 | // 7 | 8 | import SwiftGitX 9 | import XCTest 10 | 11 | final class RepositoryOperationTests: SwiftGitXTestCase { 12 | func testAdd() throws { 13 | // Create a new repository at the temporary directory 14 | let repository = Repository.mock(named: "test-add", in: Self.directory) 15 | 16 | // Create a new file in the repository 17 | let file = try repository.mockFile(named: "README.md") 18 | 19 | // Add the file to the index 20 | try repository.add(file: file) 21 | 22 | // Get status of the repository 23 | let status = try repository.status(file: file) 24 | 25 | // Check if the file is added to the index 26 | XCTAssertEqual(status, [.indexNew]) 27 | } 28 | 29 | func testCommit() throws { 30 | // Create a new repository at the temporary directory 31 | let repository = Repository.mock(named: "test-commit", in: Self.directory) 32 | 33 | // Create a new file in the repository 34 | let file = try repository.mockFile(named: "README.md") 35 | 36 | // Add the file to the index 37 | try repository.add(file: file) 38 | 39 | // Commit the changes 40 | let commit = try repository.commit(message: "Initial commit") 41 | 42 | // Get the HEAD commit 43 | let headCommit = try XCTUnwrap(repository.HEAD.target as? Commit) 44 | 45 | // Check if the HEAD commit is the same as the created commit 46 | XCTAssertEqual(commit, headCommit) 47 | } 48 | 49 | func testReset() throws { 50 | // Create a new repository at the temporary directory 51 | let repository = Repository.mock(named: "test-reset", in: Self.directory) 52 | 53 | let initialCommit = try repository.mockCommit() 54 | 55 | // Create a new file in the repository 56 | let file = try repository.mockFile(named: "ResetMe.md") 57 | 58 | // Add the file to the index 59 | try repository.add(file: file) 60 | 61 | // Reset the staged changes 62 | try repository.reset(from: initialCommit, files: [file]) 63 | 64 | // Get the status of the file 65 | let status = try repository.status(file: file) 66 | 67 | // Check if the file is reset 68 | XCTAssertEqual(status, [.workingTreeNew]) 69 | } 70 | 71 | func testResetSoft() throws { 72 | // Create a new repository at the temporary directory 73 | let repository = Repository.mock(named: "test-reset-soft", in: Self.directory) 74 | 75 | // Create mock commit 76 | let initialCommit = try repository.mockCommit() 77 | 78 | // Create a oops commit 79 | try repository.mockCommit( 80 | message: "Oops!", 81 | file: repository.mockFile(named: "Undefined", content: "Reset me!") 82 | ) 83 | 84 | // Reset the repository to the previous commit 85 | try repository.reset(to: initialCommit) 86 | 87 | // Get the HEAD commit 88 | let headCommit = try XCTUnwrap(repository.HEAD.target as? Commit) 89 | 90 | // Check if the HEAD commit is the same as the previous commit 91 | XCTAssertEqual(headCommit, initialCommit) 92 | } 93 | 94 | func testRestoreWorkingTree() throws { 95 | // Create a new repository at the temporary directory 96 | let repository = Repository.mock(named: "test-restore-working-tree", in: Self.directory) 97 | 98 | // Create a new file 99 | let fileToRestore = try repository.mockFile(named: "WorkingTree.md", content: "Hello, World!") 100 | 101 | // Commit the file 102 | try repository.mockCommit(message: "Initial commit", file: fileToRestore) 103 | 104 | // Modify the file 105 | try Data("Restore me!".utf8).write(to: fileToRestore) 106 | 107 | // Create a new file to stage (this should not be restored) 108 | let fileToStage = try repository.mockFile(named: "Stage.md", content: "Stage me!") 109 | 110 | // Stage the file 111 | try repository.add(file: fileToStage) 112 | 113 | // Restore the file to the head commit 114 | try repository.restore(paths: ["WorkingTree.md", "Stage.md"]) 115 | 116 | // Check if the file content is the same as the head commit 117 | let restoredFileContent = try String(contentsOf: fileToRestore) 118 | 119 | XCTAssertEqual(restoredFileContent, "Hello, World!") 120 | XCTAssertTrue(FileManager.default.fileExists(atPath: fileToStage.path)) 121 | } 122 | 123 | func testRestoreStage() throws { 124 | // Create a new repository at the temporary directory 125 | let repository = Repository.mock(named: "test-restore-stage", in: Self.directory) 126 | 127 | // Create a new file 128 | let workingTreeFile = try repository.mockFile(named: "WorkingTree.md", content: "Hello, World!") 129 | 130 | // Commit the file 131 | try repository.mockCommit(message: "Initial commit", file: workingTreeFile) 132 | 133 | // Modify the file (this should not be restored) 134 | try Data("Should not be restored!".utf8).write(to: workingTreeFile) 135 | 136 | // Create a new file to stage 137 | let stagedFile = try repository.mockFile(named: "Stage.md", content: "Stage me!") 138 | 139 | // Stage the file 140 | try repository.add(file: stagedFile) 141 | 142 | // Restore the staged file 143 | try repository.restore(.staged, paths: ["WorkingTree.md", "Stage.md"]) 144 | 145 | // Check the status of the staged file and content 146 | let stagedFileStatus = try repository.status(file: stagedFile) 147 | XCTAssertEqual(stagedFileStatus, [.workingTreeNew]) 148 | XCTAssertEqual(try String(contentsOf: stagedFile), "Stage me!") 149 | 150 | // Check the status of the working tree file and content 151 | let workingTreeFileStatus = try repository.status(file: workingTreeFile) 152 | XCTAssertEqual(workingTreeFileStatus, [.workingTreeModified]) 153 | XCTAssertTrue(FileManager.default.fileExists(atPath: workingTreeFile.path)) 154 | XCTAssertEqual(try String(contentsOf: workingTreeFile), "Should not be restored!") 155 | } 156 | 157 | func testRestoreWorkingTreeAndStage() throws { 158 | // Create a new repository at the temporary directory 159 | let repository = Repository.mock(named: "test-restore-working-tree-stage", in: Self.directory) 160 | 161 | // Create a mock commit 162 | try repository.mockCommit() 163 | 164 | // Modify the file which is created in mockCommit 165 | let file = try repository.mockFile(named: "README.md", content: "Restore stage area!") 166 | 167 | // Add the file to the index 168 | try repository.add(file: file) 169 | 170 | // Modify the file 171 | try Data("Restore working tree!".utf8).write(to: file) 172 | 173 | // Restore the working tree and stage 174 | try repository.restore([.workingTree, .staged], files: [file]) 175 | 176 | // Check the status of the file and the content 177 | let stagedFileStatus = try repository.status(file: file) 178 | XCTAssertTrue(stagedFileStatus.isEmpty) // There should be no changes (all changes are restored) 179 | XCTAssertEqual(try String(contentsOf: file), "Welcome to SwiftGitX!\n") 180 | 181 | // Create a new file to delete (this should be deleted) 182 | let fileToDelete = try repository.mockFile(named: "DeleteMe.md", content: "Delete me from stage area!") 183 | 184 | // Add the file to the index 185 | try repository.add(file: fileToDelete) 186 | 187 | // Modify the file 188 | try Data("Delete me from working tree!".utf8).write(to: fileToDelete) 189 | 190 | // Restore the working tree and stage 191 | try repository.restore([.workingTree, .staged], files: [fileToDelete]) 192 | 193 | // File should be deleted 194 | XCTAssertFalse(FileManager.default.fileExists(atPath: fileToDelete.path)) 195 | } 196 | 197 | func testRepositoryLog() async throws { 198 | // Create a new repository at the temporary directory 199 | let repository = Repository.mock(named: "test-log", in: Self.directory) 200 | 201 | var createdCommits = [Commit]() 202 | for index in 0 ..< 10 { 203 | // Create a commit 204 | let commit = try repository.mockCommit( 205 | message: "Commit \(index)", 206 | file: repository.mockFile(named: "README-\(index).md") 207 | ) 208 | 209 | createdCommits.append(commit) 210 | } 211 | 212 | // Get the log of the repository 213 | let commitSequence = try repository.log(from: repository.HEAD, sorting: .reverse) 214 | let logCommits = Array(commitSequence) 215 | 216 | // Check if the commits are the same 217 | XCTAssertEqual(logCommits, createdCommits) 218 | } 219 | 220 | func testRevert() throws { 221 | // Create a new repository at the temporary directory 222 | let repository = Repository.mock(named: "test-revert", in: Self.directory) 223 | 224 | let file = try repository.mockFile(named: "README.md", content: "Hello, World!") 225 | 226 | // Create initial commit 227 | try repository.mockCommit(message: "Initial commit", file: file) 228 | 229 | // Modify the file 230 | try Data("Revert me!".utf8).write(to: file) 231 | 232 | // Create a new commit 233 | let commitToRevert = try repository.mockCommit(message: "Second commit", file: file) 234 | 235 | // Revert the commit 236 | try repository.revert(commitToRevert) 237 | 238 | // Check the status of the file 239 | XCTAssertEqual(try repository.status(file: file), [.indexModified]) 240 | 241 | // Check the content of the file 242 | XCTAssertEqual(try String(contentsOf: file), "Hello, World!") 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /Tests/SwiftGitXTests/RepositoryTests/RepositoryPropertyTests.swift: -------------------------------------------------------------------------------- 1 | import SwiftGitX 2 | import XCTest 3 | 4 | final class RepositoryPropertyTests: SwiftGitXTestCase { 5 | func testRepositoryHEAD() throws { 6 | // Create a new repository at the temporary directory 7 | let repository = Repository.mock(named: "test-head", in: Self.directory) 8 | 9 | // Commit the file 10 | try repository.mockCommit() 11 | 12 | // Get the HEAD reference 13 | let head = try repository.HEAD 14 | 15 | // Check the HEAD reference 16 | XCTAssertEqual(head.name, "main") 17 | XCTAssertEqual(head.fullName, "refs/heads/main") 18 | } 19 | 20 | func testRepositoryHEADUnborn() throws { 21 | // Create a new repository at the temporary directory 22 | let repository = Repository.mock(named: "test-head-unborn", in: Self.directory) 23 | 24 | XCTAssertTrue(repository.isHEADUnborn) 25 | 26 | XCTAssertThrowsError(try repository.HEAD) 27 | } 28 | 29 | func testRepositoryWorkingDirectory() throws { 30 | // Create a new repository at the temporary directory 31 | let repository = Repository.mock(named: "test-working-directory", in: Self.directory) 32 | 33 | // Get the working directory of the repository 34 | let repositoryWorkingDirectory = try XCTUnwrap(repository.workingDirectory) 35 | 36 | // Get the path of the mock repository directory 37 | let expectedDirectory = if Self.directory.isEmpty { 38 | URL.temporaryDirectory.appending(components: "SwiftGitXTests", "test-working-directory/") 39 | } else { 40 | URL.temporaryDirectory.appending(components: "SwiftGitXTests", Self.directory, "test-working-directory/") 41 | } 42 | 43 | // Check if the working directory is the same as the expected directory 44 | XCTAssertEqual(repositoryWorkingDirectory.resolvingSymlinksInPath(), expectedDirectory) 45 | } 46 | 47 | func testRepositoryPath() throws { 48 | // Create a new repository at the temporary directory 49 | let repository = Repository.mock(named: "test-path", in: Self.directory) 50 | 51 | // Get the path of the mock repository directory 52 | let expectedDirectory = if Self.directory.isEmpty { 53 | URL.temporaryDirectory.appending(components: "SwiftGitXTests", "test-path/.git/") 54 | } else { 55 | URL.temporaryDirectory.appending(components: "SwiftGitXTests", Self.directory, "test-path/.git/") 56 | } 57 | 58 | // Check if the path is the same as the expected directory 59 | XCTAssertEqual(repository.path.resolvingSymlinksInPath(), expectedDirectory) 60 | } 61 | 62 | func testRepositoryPath_Bare() { 63 | // Create a new repository at the temporary directory 64 | let repository = Repository.mock(named: "test-path-bare", in: Self.directory, isBare: true) 65 | 66 | // Get the path of the mock repository directory 67 | let expectedDirectory = if Self.directory.isEmpty { 68 | URL.temporaryDirectory.appending(components: "SwiftGitXTests", "test-path-bare/") 69 | } else { 70 | URL.temporaryDirectory.appending(components: "SwiftGitXTests", Self.directory, "test-path-bare/") 71 | } 72 | 73 | // Check if the path is the same as the expected directory 74 | XCTAssertEqual(repository.path.resolvingSymlinksInPath(), expectedDirectory) 75 | } 76 | 77 | func testRepositoryIsEmpty() throws { 78 | // Create a new repository at the temporary directory 79 | let repository = Repository.mock(named: "test-is-empty", in: Self.directory) 80 | 81 | // Check if the repository is empty 82 | XCTAssertTrue(repository.isEmpty) 83 | 84 | // Create a commit 85 | _ = try repository.mockCommit() 86 | 87 | // Check if the repository is not empty 88 | XCTAssertFalse(repository.isEmpty) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Tests/SwiftGitXTests/RepositoryTests/RepositoryRemoteOperationTests.swift: -------------------------------------------------------------------------------- 1 | import SwiftGitX 2 | import XCTest 3 | 4 | final class RepositoryRemoteOperationTests: SwiftGitXTestCase { 5 | func testRepositoryPush() async throws { 6 | // Create a mock repository at the temporary directory 7 | let source = URL(string: "https://github.com/ibrahimcetin/ibrahimcetin.dev.git")! 8 | let remoteDirectory = Repository.mockDirectory(named: "test-push--remote", in: Self.directory) 9 | let remoteRepository = try await Repository.clone(from: source, to: remoteDirectory, options: .bare) 10 | 11 | // Clone the remote repository to the local repository 12 | let localDirectory = Repository.mockDirectory(named: "test-push--local", in: Self.directory) 13 | let localRepository = try await Repository.clone(from: remoteDirectory, to: localDirectory) 14 | 15 | // Create a new commit in the local repository 16 | try localRepository.mockCommit(message: "Pushed commit", file: localRepository.mockFile(named: "PushedFile.md")) 17 | 18 | // Push the commit to the remote repository 19 | try await localRepository.push() 20 | 21 | // Check if the commit is pushed 22 | try XCTAssertEqual(localRepository.HEAD.target.id, remoteRepository.HEAD.target.id) 23 | } 24 | 25 | func testRepositoryPushEmptyRemote_SetUpstream() async throws { 26 | // Create a mock repository at the temporary directory 27 | let remoteRepository = Repository.mock(named: "test-push-empty--remote", in: Self.directory, isBare: true) 28 | 29 | // Create a mock repository at the temporary directory 30 | let localRepository = Repository.mock(named: "test-push-empty--local", in: Self.directory) 31 | 32 | // Create a new commit in the local repository 33 | try localRepository.mockCommit(message: "Pushed commit", file: localRepository.mockFile(named: "PushedFile.md")) 34 | 35 | // Add remote repository to the local repository 36 | try localRepository.remote.add(named: "origin", at: remoteRepository.path) 37 | 38 | // Push the commit to the remote repository 39 | try await localRepository.push() 40 | 41 | // Check if the commit is pushed 42 | try XCTAssertEqual(localRepository.HEAD.target.id, remoteRepository.HEAD.target.id) 43 | 44 | // Upstream branch should be nil 45 | try XCTAssertNil(localRepository.branch.current.upstream) 46 | 47 | // Set the upstream branch 48 | try localRepository.branch.setUpstream(to: localRepository.branch.get(named: "origin/main")) 49 | 50 | // Check if the upstream branch is set 51 | let upstreamBranch = try XCTUnwrap(localRepository.branch.current.upstream as? Branch) 52 | XCTAssertEqual(upstreamBranch.target.id, try remoteRepository.HEAD.target.id) 53 | XCTAssertEqual(upstreamBranch.name, "origin/main") 54 | XCTAssertEqual(upstreamBranch.fullName, "refs/remotes/origin/main") 55 | } 56 | 57 | func testRepositoryFetch() async throws { 58 | // Create remote repository 59 | let remoteRepository = Repository.mock(named: "test-fetch--remote", in: Self.directory) 60 | 61 | // Create mock commit in the remote repository 62 | try remoteRepository.mockCommit() 63 | 64 | // Create local repository 65 | let localRepository = Repository.mock(named: "test-fetch--local", in: Self.directory) 66 | 67 | // Add remote repository to the local repository 68 | try localRepository.remote.add(named: "origin", at: remoteRepository.workingDirectory) 69 | 70 | // Fetch the commit from the remote repository 71 | try await localRepository.fetch() 72 | 73 | // Check if the remote branch is fetched 74 | let remoteBranch = try localRepository.branch.get(named: "origin/main") 75 | try XCTAssertEqual(remoteBranch.target.id, remoteRepository.HEAD.target.id) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Tests/SwiftGitXTests/RepositoryTests/RepositoryShowTests.swift: -------------------------------------------------------------------------------- 1 | @testable import SwiftGitX 2 | import XCTest 3 | 4 | final class RepositoryShowTests: SwiftGitXTestCase { 5 | func testShowCommit() throws { 6 | // Create mock repository at the temporary directory 7 | let repository = Repository.mock(named: "test-show-commit", in: Self.directory) 8 | 9 | // Create a new commit 10 | let commit = try repository.mockCommit() 11 | 12 | // Get the commit by id 13 | let commitShowed: Commit = try repository.show(id: commit.id) 14 | 15 | // Check if the commit is the same 16 | XCTAssertEqual(commit, commitShowed) 17 | } 18 | 19 | func testShowTag() throws { 20 | // Create mock repository at the temporary directory 21 | let repository = Repository.mock(named: "test-show-tag", in: Self.directory) 22 | 23 | // Create a new commit 24 | let commit = try repository.mockCommit() 25 | 26 | // Create a new tag 27 | let tag = try repository.tag.create(named: "v1.0.0", target: commit) 28 | 29 | // Get the tag by id 30 | let tagShowed: Tag = try repository.show(id: tag.id) 31 | 32 | // Check if the tag is the same 33 | XCTAssertEqual(tag, tagShowed) 34 | } 35 | 36 | func testShowTree() throws { 37 | // Create mock repository at the temporary directory 38 | let repository = Repository.mock(named: "test-show-tree", in: Self.directory) 39 | 40 | // Create a new commit 41 | let commit = try repository.mockCommit() 42 | 43 | // Get the tree of the commit 44 | let tree = commit.tree 45 | 46 | // Get the tree by id 47 | let treeShowed: Tree = try repository.show(id: tree.id) 48 | 49 | // Check if the tree is the same 50 | XCTAssertEqual(tree, treeShowed) 51 | } 52 | 53 | func testShowBlob() throws { 54 | // Create mock repository at the temporary directory 55 | let repository = Repository.mock(named: "test-show-blob", in: Self.directory) 56 | 57 | // Create a new commit 58 | let commit = try repository.mockCommit() 59 | 60 | // Get the blob of the file 61 | let blob = try XCTUnwrap(commit.tree.entries.first) 62 | 63 | // Get the blob by id 64 | let blobShowed: Blob = try repository.show(id: blob.id) 65 | 66 | // Check if the blob properties are the same 67 | XCTAssertEqual(blob.id, blobShowed.id) 68 | XCTAssertEqual(blob.type, blobShowed.type) 69 | } 70 | 71 | func testShowInvalidObjectType() throws { 72 | // Create mock repository at the temporary directory 73 | let repository = Repository.mock(named: "test-show-invalid-object-type", in: Self.directory) 74 | 75 | // Create a new commit 76 | let commit = try repository.mockCommit() 77 | 78 | // Try to show a commit as a tree 79 | XCTAssertThrowsError(try repository.show(id: commit.id) as Tree) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Tests/SwiftGitXTests/RepositoryTests/RepositorySwitchTests.swift: -------------------------------------------------------------------------------- 1 | import SwiftGitX 2 | import XCTest 3 | 4 | final class RepositorySwitchTests: SwiftGitXTestCase { 5 | func testRepositorySwitchBranch() throws { 6 | // Create a new repository at the temporary directory 7 | let repository = Repository.mock(named: "test-switch-branch", in: Self.directory) 8 | 9 | // Create mock commit 10 | let commit = try repository.mockCommit() 11 | 12 | // Create a new branch 13 | let branch = try repository.branch.create(named: "feature", target: commit) 14 | 15 | // Switch the new branch 16 | XCTAssertNoThrow(try repository.switch(to: branch)) 17 | 18 | // Get the HEAD reference 19 | let head = try repository.HEAD 20 | 21 | // Check the HEAD reference 22 | XCTAssertEqual(head.name, branch.name) 23 | XCTAssertEqual(head.fullName, branch.fullName) 24 | } 25 | 26 | func testRepositorySwitchBranchGuess() async throws { 27 | let source = URL(string: "https://github.com/ibrahimcetin/PassbankMD.git")! 28 | let repositoryDirectory = Repository.mockDirectory(named: "test-switch-branch-guess", in: Self.directory) 29 | let repository = try await Repository.clone(from: source, to: repositoryDirectory) 30 | 31 | // Switch to the branch 32 | let remoteBranch = try repository.branch.get(named: "origin/fixes-for-kivymd-1.2") 33 | try repository.switch(to: remoteBranch) 34 | 35 | // Get the HEAD reference 36 | let head = try repository.HEAD 37 | 38 | // Check the HEAD reference 39 | XCTAssertEqual(head.name, remoteBranch.name.replacingOccurrences(of: "origin/", with: "")) 40 | XCTAssertEqual(head.target as? Commit, remoteBranch.target as? Commit) 41 | } 42 | 43 | func testRepositorySwitchCommit() throws { 44 | // Create a new repository at the temporary directory 45 | let repository = Repository.mock(named: "test-switch-commit", in: Self.directory) 46 | 47 | // Create mock commit 48 | let commit = try repository.mockCommit() 49 | 50 | // Switch to the commit 51 | XCTAssertNoThrow(try repository.switch(to: commit)) 52 | 53 | // Get the HEAD reference 54 | let head = try repository.HEAD 55 | 56 | // Check the HEAD reference (detached HEAD) 57 | XCTAssertTrue(repository.isHEADDetached) 58 | 59 | XCTAssertEqual(head.name, "HEAD") 60 | XCTAssertEqual(head.fullName, "HEAD") 61 | } 62 | 63 | func testRepositorySwitchTagAnnotated() throws { 64 | // Create a new repository at the temporary directory 65 | let repository = Repository.mock(named: "test-switch-tag-annotated", in: Self.directory) 66 | 67 | // Create mock commit 68 | let commit = try repository.mockCommit() 69 | 70 | // Create a new tag 71 | let tag = try repository.tag.create(named: "v1.0.0", target: commit) 72 | 73 | // Switch to the tag 74 | XCTAssertNoThrow(try repository.switch(to: tag)) 75 | 76 | // Get the HEAD reference 77 | let head = try repository.HEAD 78 | 79 | // Check the HEAD reference 80 | XCTAssertEqual(head.name, tag.name) 81 | XCTAssertEqual(head.fullName, tag.fullName) 82 | } 83 | 84 | func testRepositorySwitchTagLightweight() throws { 85 | // Create a new repository at the temporary directory 86 | let repository = Repository.mock(named: "test-switch-tag-lightweight", in: Self.directory) 87 | 88 | // Create mock commit 89 | let commit = try repository.mockCommit() 90 | 91 | // Create a new tag 92 | let tag = try repository.tag.create(named: "v1.0.0", target: commit, type: .lightweight) 93 | 94 | // When a lightweight tag is created, the tag ID is the same as the commit ID 95 | XCTAssertEqual(tag.id, commit.id) 96 | 97 | // Switch to the tag 98 | XCTAssertNoThrow(try repository.switch(to: tag)) 99 | 100 | // Get the HEAD reference 101 | let head = try repository.HEAD 102 | 103 | // Check the HEAD reference 104 | XCTAssertEqual(head.target.id, tag.id) 105 | 106 | XCTAssertEqual(head.name, tag.name) 107 | XCTAssertEqual(head.fullName, tag.fullName) 108 | } 109 | 110 | func testRepositorySwitchTagLightweightTreeFailure() throws { 111 | // Create a new repository at the temporary directory 112 | let repository = Repository.mock(named: "test-switch-tag-lightweight-tree-failure", in: Self.directory) 113 | 114 | // Create mock commit 115 | let commit = try repository.mockCommit() 116 | 117 | // Create a new tag 118 | let tag = try repository.tag.create(named: "v1.0.0", target: commit.tree, type: .lightweight) 119 | 120 | // Switch to the tag 121 | XCTAssertThrowsError(try repository.switch(to: tag)) 122 | } 123 | 124 | // TODO: Add test for remote branch checkout 125 | } 126 | -------------------------------------------------------------------------------- /Tests/SwiftGitXTests/SwiftGitXTests.swift: -------------------------------------------------------------------------------- 1 | import SwiftGitX 2 | import Testing 3 | import XCTest 4 | 5 | class SwiftGitXTestCase: XCTestCase { 6 | static var directory: String { 7 | String(describing: Self.self) 8 | } 9 | 10 | override class func setUp() { 11 | super.setUp() 12 | 13 | // Initialize the SwiftGitX library 14 | XCTAssertNoThrow(try SwiftGitX.initialize()) 15 | } 16 | 17 | override class func tearDown() { 18 | // Shutdown the SwiftGitX library 19 | XCTAssertNoThrow(try SwiftGitX.shutdown()) 20 | 21 | // Remove the temporary directory for the tests 22 | try? FileManager.default.removeItem(at: Repository.testsDirectory.appending(component: directory)) 23 | 24 | super.tearDown() 25 | } 26 | } 27 | 28 | /// Base class for SwiftGitX tests to initialize and shutdown the library 29 | /// 30 | /// - Important: Inherit from this class to create a test suite. 31 | class SwiftGitXTest { 32 | static var directory: String { 33 | String(describing: Self.self) 34 | } 35 | 36 | init() throws { 37 | try SwiftGitX.initialize() 38 | } 39 | 40 | deinit { 41 | _ = try? SwiftGitX.shutdown() 42 | } 43 | } 44 | 45 | // Test the SwiftGitX struct to initialize and shutdown the library 46 | @Suite("SwiftGitX Tests", .tags(.swiftGitX), .serialized) 47 | struct SwiftGitXTests { 48 | @Test("Test SwiftGitX Initialize") 49 | func testSwiftGitXInitialize() async throws { 50 | // Initialize the SwiftGitX library 51 | let count = try SwiftGitX.initialize() 52 | 53 | // Check if the initialization count is valid 54 | #expect(count > 0) 55 | } 56 | 57 | @Test("Test SwiftGitX Shutdown") 58 | func testSwiftGitXShutdown() async throws { 59 | // Shutdown the SwiftGitX library 60 | let count = try SwiftGitX.shutdown() 61 | 62 | // Check if the shutdown count is valid 63 | #expect(count >= 0) 64 | } 65 | 66 | @Test("Test SwiftGitX Version") 67 | func testVersion() throws { 68 | // Get the libgit2 version 69 | let version = SwiftGitX.libgit2Version 70 | 71 | // Check if the version is valid 72 | #expect(version == "1.8.0") 73 | } 74 | } 75 | 76 | extension Testing.Tag { 77 | @Tag static var swiftGitX: Self 78 | } 79 | --------------------------------------------------------------------------------