├── .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 |
--------------------------------------------------------------------------------