├── Sources ├── Clibgit2 │ ├── Clibgit2.h │ └── module.modulemap └── Git │ ├── Git.swift │ ├── Credentials.swift │ ├── Message.swift │ ├── References │ ├── Note.swift │ ├── Branch.swift │ └── Tag.swift │ ├── Objects │ ├── Blob.swift │ ├── Tree.swift │ └── Commit.swift │ ├── Extensions │ └── Array+Extensions.swift │ ├── Repository │ ├── Head.swift │ ├── Attributes.swift │ ├── Clone.swift │ ├── Index.swift │ └── Checkout.swift │ ├── Error.swift │ ├── Reference.swift │ ├── Signature.swift │ ├── Remote.swift │ ├── RevisionWalker.swift │ ├── Object.swift │ └── Repository.swift ├── Tests ├── LinuxMain.swift └── GitTests │ └── GitTests.swift ├── .gitignore ├── .github └── workflows │ ├── documentation.yml │ └── ci.yml ├── Package.swift ├── LICENSE.md └── README.md /Sources/Clibgit2/Clibgit2.h: -------------------------------------------------------------------------------- 1 | #include "git2.h" 2 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | fatalError("Run with `swift test --enable-test-discovery`") 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | .swiftpm 7 | -------------------------------------------------------------------------------- /Sources/Clibgit2/module.modulemap: -------------------------------------------------------------------------------- 1 | module Clibgit2 [system] { 2 | header "Clibgit2.h" 3 | link "git2" 4 | export * 5 | } 6 | -------------------------------------------------------------------------------- /Sources/Git/Git.swift: -------------------------------------------------------------------------------- 1 | import Clibgit2 2 | 3 | let initializer: () = { 4 | git_libgit2_init() 5 | }() 6 | 7 | 8 | #if os(Windows) 9 | let pathListSeparator = ";" 10 | #else 11 | let pathListSeparator = ":" 12 | #endif 13 | -------------------------------------------------------------------------------- /Sources/Git/Credentials.swift: -------------------------------------------------------------------------------- 1 | import Clibgit2 2 | 3 | public enum Credentials { 4 | case `default` 5 | case plaintext(username: String, password: String) 6 | case sshAgent(username: String) 7 | case sshMemory(username: String, publicKey: String, privateKey: String, passphrase: String) 8 | } 9 | -------------------------------------------------------------------------------- /Sources/Git/Message.swift: -------------------------------------------------------------------------------- 1 | import Clibgit2 2 | 3 | public enum Message { 4 | public func prettify(_ message: String, stripComments: Bool = false, commentDelimiter: Character = "#") throws -> String { 5 | var buffer = git_buf() 6 | defer { git_buf_free(&buffer) } 7 | 8 | try attempt { git_message_prettify(&buffer, message, stripComments ? 1 : 0, numericCast(commentDelimiter.asciiValue ?? 35)) } 9 | 10 | return String(cString: buffer.ptr) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Git/References/Note.swift: -------------------------------------------------------------------------------- 1 | import Clibgit2 2 | 3 | /// A note attached to a commit. 4 | public final class Note: Reference { 5 | public var message: String! { 6 | return String(validatingUTF8: git_note_message(pointer)) 7 | } 8 | 9 | public var author: Signature { 10 | return Signature(rawValue: git_commit_author(pointer).pointee) 11 | } 12 | 13 | public var committer: Signature { 14 | return Signature(rawValue: git_commit_committer(pointer).pointee) 15 | } 16 | 17 | /// The target of the reference. 18 | public override var target: Object? { 19 | try? owner.lookup(type: Object.self, with: Object.ID { oid in 20 | git_note_id(pointer) 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - .github/workflows/documentation.yml 9 | - Sources/**.swift 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v1 18 | - name: Generate Documentation 19 | uses: SwiftDocOrg/swift-doc@master 20 | with: 21 | inputs: "Sources" 22 | output: "Documentation" 23 | - name: Upload Documentation to Wiki 24 | uses: SwiftDocOrg/github-wiki-publish-action@v1 25 | with: 26 | path: "Documentation" 27 | env: 28 | GH_PERSONAL_ACCESS_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} 29 | -------------------------------------------------------------------------------- /Sources/Git/Objects/Blob.swift: -------------------------------------------------------------------------------- 1 | import Clibgit2 2 | import Foundation 3 | 4 | /// A file revision object. 5 | public final class Blob: Object { 6 | class override var type: git_object_t { return GIT_OBJECT_BLOB } 7 | 8 | /// The repository containing the blob. 9 | public override var owner: Repository { 10 | return Repository(git_blob_owner(pointer)) 11 | } 12 | 13 | /// Whether the blob is binary. 14 | public var isBinary: Bool { 15 | git_blob_is_binary(pointer) != 0 16 | } 17 | 18 | /// The size in bytes of the content 19 | public var size: Int { 20 | Int(git_blob_rawsize(pointer)) 21 | } 22 | 23 | /// The blob contents. 24 | public var data: Data { 25 | return Data(bytes: git_blob_rawcontent(pointer), count: size) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Git/Extensions/Array+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Clibgit2 2 | import Foundation 3 | 4 | extension Array where Element == String { 5 | init(_ git_strarray: git_strarray) { 6 | self.init((0..(_ body: (git_strarray) throws -> T) rethrows -> T { 11 | let cStrings = UnsafeMutablePointer?>.allocate(capacity: count) 12 | defer { cStrings.deallocate() } 13 | 14 | for (index, string) in enumerated() { 15 | cStrings.advanced(by: index).pointee = strdup(string.cString(using: .utf8)!) 16 | } 17 | 18 | return try body(git_strarray(strings: cStrings, count: count)) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 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: "Git", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "Git", 12 | targets: ["Git"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | .systemLibrary(name: "Clibgit2", pkgConfig: "libgit2", providers: [ 20 | .brew(["libgit2"]) 21 | ]), 22 | .target( 23 | name: "Git", 24 | dependencies: ["Clibgit2"]), 25 | .testTarget( 26 | name: "GitTests", 27 | dependencies: ["Git"]), 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /Sources/Git/Repository/Head.swift: -------------------------------------------------------------------------------- 1 | import Clibgit2 2 | import Foundation 3 | 4 | extension Repository { 5 | /// The repository `HEAD` reference. 6 | public enum Head: Equatable { 7 | case attached(Branch) 8 | case detached(Commit) 9 | 10 | public var attached: Bool { 11 | switch self { 12 | case .attached: 13 | return true 14 | case .detached: 15 | return false 16 | } 17 | } 18 | 19 | public var detached: Bool { 20 | return !attached 21 | } 22 | 23 | public var branch: Branch? { 24 | switch self { 25 | case .attached(let branch): 26 | return branch 27 | case .detached: 28 | return nil 29 | } 30 | } 31 | 32 | public var commit: Commit? { 33 | switch self { 34 | case .attached(let branch): 35 | return branch.commit 36 | case .detached(let commit): 37 | return commit 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2020 Read Evaluate Press, LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Sources/Git/Error.swift: -------------------------------------------------------------------------------- 1 | import Clibgit2 2 | 3 | /// A libgit error. 4 | public struct Error: Swift.Error { 5 | 6 | /** 7 | The error code. 8 | 9 | Error codes correspond to `git_error_code` constants, like `GIT_ENOTFOUND`. 10 | */ 11 | public let code: Int // TODO: import raw values declared by libgit2 12 | 13 | 14 | /// The error message, if any. 15 | public let message: String? 16 | 17 | private static var lastErrorMessage: String? { 18 | guard let error = giterr_last() else { return nil } 19 | return String(cString: error.pointee.message) 20 | } 21 | 22 | init(code: Code, message: String? = Error.lastErrorMessage) { 23 | self.code = Int(code) 24 | self.message = message 25 | } 26 | } 27 | 28 | // MARK: - 29 | 30 | func attempt(throwing function: () -> Int32) throws { 31 | _ = initializer // FIXME 32 | let result = function() 33 | guard result == 0 else { 34 | throw Error(code: result) 35 | } 36 | } 37 | 38 | func result(of function: () -> Int32) -> Result { 39 | do { 40 | try attempt(throwing: function) 41 | return .success(()) 42 | } catch { 43 | return .failure(error) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | macos: 11 | runs-on: macos-latest 12 | 13 | strategy: 14 | matrix: 15 | xcode: 16 | - "12" # Swift 5.3 17 | 18 | name: "macOS (Xcode ${{ matrix.xcode }})" 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v1 23 | - name: Install libgit2 24 | run: brew install libgit2 25 | - name: Build and Test 26 | run: swift test 27 | env: 28 | DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer 29 | 30 | linux: 31 | runs-on: ubuntu-latest 32 | 33 | strategy: 34 | matrix: 35 | swift: ["5.3"] 36 | 37 | name: "Linux (Swift ${{ matrix.swift }})" 38 | 39 | container: 40 | image: swift:${{ matrix.swift }} 41 | 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@v1 45 | - name: Install libgit2 46 | run: | 47 | apt-get -qq update 48 | apt-get install -y curl unzip cmake libssl-dev libssh2-1-dev python 49 | curl -L -o libgit2.zip https://github.com/libgit2/libgit2/releases/download/v1.0.1/libgit2-1.0.1.zip 50 | unzip -q libgit2.zip 51 | cd libgit2-1.0.1 52 | ls 53 | cmake . -DCMAKE_INSTALL_PREFIX=/usr 54 | cmake --build . --target install 55 | - name: Build and Test 56 | run: swift test --enable-test-discovery 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Git 2 | 3 | A package for working with Git repositories in Swift, 4 | built on top of [libgit2](https://libgit2.org). 5 | 6 | > **Warning**: 7 | > This is currently a work-in-progress and shouldn't be used in production. 8 | 9 | ## Requirements 10 | 11 | - Swift 5.2 12 | 13 | ## Usage 14 | 15 | ```swift 16 | import Git 17 | import Foundation 18 | 19 | let remoteURL = URL(string: "https://github.com/SwiftDocOrg/StringLocationConverter.git")! 20 | let localURL = URL(fileURLWithPath: "<#path/to/repository#>") 21 | let repository = try Repository.clone(from: remoteURL, to: localURL) 22 | 23 | repository.head?.name // "refs/heads/master" 24 | 25 | let master = try repository.branch(named: "refs/heads/master") 26 | master?.shortName// "master" 27 | master?.commit?.message // "Initial commit" 28 | master?.commit?.id.description // "6cf6579c191e20a5a77a7e3176d37a8d654c9fc4" 29 | master?.commit?.author.name // "Mattt" 30 | 31 | let tree = master?.commit?.tree 32 | tree.count // 6 33 | 34 | tree?["README.md"]?.object is Blob // true 35 | let blob = tree?["README.md"]?.object as? Blob 36 | String(data: blob!.data, encoding: .utf8) // "# StringLocationConverter (...)" 37 | 38 | tree?["Sources"]?.object is Tree // true 39 | let subtree = tree?["Sources"]?.object as? Tree 40 | subtree?.count // 1 41 | subtree?["StringLocationConverter"]?.object is Tree // true 42 | 43 | 44 | let index = repository.index 45 | let entries = index?.entries.compactMap { $0.blob } 46 | entries?.count // 9 47 | ``` 48 | 49 | ## License 50 | 51 | MIT 52 | 53 | ## Contact 54 | 55 | Mattt ([@mattt](https://twitter.com/mattt)) 56 | -------------------------------------------------------------------------------- /Sources/Git/References/Branch.swift: -------------------------------------------------------------------------------- 1 | import Clibgit2 2 | 3 | /// A named reference to a commit. 4 | public final class Branch: Reference { 5 | /// The referenced commit. 6 | public var commit: Commit? { 7 | let id: Object.ID 8 | switch git_reference_type(pointer) { 9 | case GIT_REFERENCE_SYMBOLIC: 10 | var resolved: OpaquePointer? 11 | guard case .success = result(of: { git_reference_resolve(&resolved, pointer) }) else { return nil } 12 | defer { git_reference_free(resolved) } 13 | id = Object.ID(rawValue: git_reference_target(resolved).pointee) 14 | default: 15 | id = Object.ID(rawValue: git_reference_target(pointer).pointee) 16 | } 17 | 18 | return try? owner.lookup(type: Commit.self, with: id) 19 | } 20 | 21 | /// The short name of the branch. 22 | public var shortName: String { 23 | var pointer: UnsafePointer? 24 | guard case .success = result(of: { git_branch_name(&pointer, self.pointer) }), 25 | let string = String(validatingUTF8: pointer!) 26 | else { return name } 27 | 28 | return string 29 | } 30 | 31 | /// Whether `HEAD` points to the branch. 32 | public var isHEAD: Bool { 33 | return git_branch_is_head(pointer) != 0 34 | } 35 | 36 | /// Whether the branch is checked out. 37 | public var isCheckedOut: Bool { 38 | return git_branch_is_checked_out(pointer) != 0 39 | } 40 | 41 | /// Whether the branch is a remote tracking branch. 42 | public var isRemote: Bool { 43 | return git_reference_is_remote(pointer) != 0 44 | } 45 | 46 | /// Whether the branch is local. 47 | public var isLocal: Bool { 48 | return !isRemote 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Git/References/Tag.swift: -------------------------------------------------------------------------------- 1 | import Clibgit2 2 | 3 | /// A named reference to an object or another reference. 4 | public final class Tag: Reference { 5 | /// The tag name. 6 | public override var name: String { 7 | return String(validatingUTF8: git_tag_name(pointer))! 8 | } 9 | 10 | /// The target of the reference. 11 | public override var target: Object? { 12 | var target: OpaquePointer? 13 | guard case .success = result(of: { git_tag_target(&target, self.pointer) }) else { return nil } 14 | return Object.type(of: git_tag_target_type(self.pointer))?.init(target!) 15 | } 16 | } 17 | 18 | // MARK: - 19 | 20 | extension Tag { 21 | /// A tag annotation. 22 | public final class Annotation: Object { 23 | class override var type: git_object_t { return GIT_OBJECT_TAG } 24 | 25 | /// The target of the reference. 26 | var target: Object? { 27 | var target: OpaquePointer? 28 | guard case .success = result(of: { git_tag_target(&target, self.pointer) }) else { return nil } 29 | return Object.type(of: git_tag_target_type(self.pointer))?.init(target!) 30 | } 31 | 32 | /// The signature of the tag author. 33 | public var tagger: Signature { 34 | return Signature(rawValue: git_tag_tagger(pointer).pointee) 35 | } 36 | 37 | /// The tag message, if any. 38 | public var message: String? { 39 | return String(validatingUTF8: git_tag_message(pointer)) 40 | } 41 | 42 | public func peel() throws -> T? { 43 | var pointer: OpaquePointer? 44 | try attempt { git_tag_peel(&pointer, self.pointer) } 45 | return T(pointer!) 46 | } 47 | } 48 | 49 | // /// The tag's annotation, if any. 50 | // public var annotation: Annotation? { 51 | // guard let id = target else { return nil } 52 | // return try? owner.lookup(id) 53 | // } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /Sources/Git/Objects/Tree.swift: -------------------------------------------------------------------------------- 1 | import Clibgit2 2 | 3 | /// A tree (directory listing) object. 4 | public final class Tree: Object { 5 | class override var type: git_object_t { return GIT_OBJECT_TREE } 6 | 7 | /// The repository containing the tree. 8 | public override var owner: Repository { 9 | return Repository(git_tree_owner(pointer)) 10 | } 11 | 12 | /// A tree entry. 13 | public final class Entry { 14 | weak var tree: Tree? 15 | 16 | var pointer: OpaquePointer 17 | 18 | /// The object corresponding to the tree entry. 19 | public var object: Object? { 20 | var pointer: OpaquePointer? 21 | guard let tree = tree, 22 | case .success = result(of: { git_tree_entry_to_object(&pointer, tree.owner.pointer, self.pointer) }) 23 | else { return nil } 24 | 25 | return Object.type(of: git_object_type(pointer!))?.init(pointer!) 26 | } 27 | 28 | /// The file attributes of a tree entry 29 | public var attributes: Int32 { 30 | return Int32(git_tree_entry_filemode(pointer).rawValue) 31 | } 32 | 33 | /// The filename of the tree entry. 34 | public var name: String { 35 | return String(validatingUTF8: git_tree_entry_name(pointer))! 36 | } 37 | 38 | required init(_ pointer: OpaquePointer) { 39 | self.pointer = pointer 40 | } 41 | 42 | convenience init(in tree: Tree, at index: Int) { 43 | self.init(git_tree_entry_byindex(tree.pointer, index)) 44 | self.tree = tree 45 | } 46 | } 47 | 48 | public subscript(_ name: String) -> Entry? { 49 | return indices.lazy 50 | .map { Entry(in: self, at: $0) } 51 | .first(where: { $0.name == name }) 52 | } 53 | } 54 | 55 | // MARK: - Hashable 56 | 57 | extension Tree.Entry: Hashable { 58 | public static func == (lhs: Tree.Entry, rhs: Tree.Entry) -> Bool { 59 | (lhs.name, lhs.object?.id) == (rhs.name, rhs.object?.id) 60 | } 61 | 62 | public func hash(into hasher: inout Hasher) { 63 | hasher.combine(object?.id) 64 | } 65 | } 66 | 67 | // MARK: - RandomAccessCollection 68 | 69 | extension Tree: RandomAccessCollection { 70 | public typealias Element = Entry 71 | 72 | public var startIndex: Int { 0 } 73 | public var endIndex: Int { git_tree_entrycount(pointer) } 74 | 75 | public subscript(_ index: Int) -> Entry { 76 | precondition(indices.contains(index)) 77 | return Entry(in: self, at: index) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/Git/Objects/Commit.swift: -------------------------------------------------------------------------------- 1 | import Clibgit2 2 | 3 | /// A commit object. 4 | public final class Commit: Object { 5 | class override var type: git_object_t { return GIT_OBJECT_COMMIT } 6 | 7 | /// The repository containing the commit. 8 | public override var owner: Repository { 9 | return Repository(git_commit_owner(pointer)) 10 | } 11 | 12 | /// The tree containing the commit, if any. 13 | public var tree: Tree? { 14 | let id = Object.ID(rawValue: git_commit_tree_id(pointer).pointee) 15 | return try? owner.lookup(type: Tree.self, with: id) 16 | } 17 | 18 | /// The parents of the commit. 19 | public var parents: [Commit] { 20 | var parents: [Commit] = [] 21 | for n in 0.. (ahead: Int, behind: Int) { 53 | var ahead: Int = 0, behind: Int = 0 54 | var localOID = self.id.rawValue, upstreamOID = upstream.id.rawValue 55 | try attempt { git_graph_ahead_behind(&ahead, &behind, pointer, &localOID, &upstreamOID) } 56 | return (ahead, behind) 57 | } 58 | 59 | /** 60 | Determines whether the commit is a descendent of another commit. 61 | 62 | - Parameters: 63 | - ancestor: The presumptive ancestor. 64 | */ 65 | public func isDescendent(of ancestor: Commit) throws -> Bool { 66 | var commitOID = self.id.rawValue, ancestorOID = ancestor.id.rawValue 67 | let result = git_graph_descendant_of(owner.pointer, &commitOID, &ancestorOID) 68 | switch result { 69 | case 0: return false 70 | case 1: return true 71 | case let code: 72 | throw Error(code: code) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/Git/Repository/Attributes.swift: -------------------------------------------------------------------------------- 1 | import Clibgit2 2 | import Foundation 3 | 4 | extension Repository { 5 | // /** 6 | // * Check attribute flags: Reading values from index and working directory. 7 | // * 8 | // * When checking attributes, it is possible to check attribute files 9 | // * in both the working directory (if there is one) and the index (if 10 | // * there is one). You can explicitly choose where to check and in 11 | // * which order using the following flags. 12 | // * 13 | // * Core git usually checks the working directory then the index, 14 | // * except during a checkout when it checks the index first. It will 15 | // * use index only for creating archives or for a bare repo (if an 16 | // * index has been specified for the bare repo). 17 | // */ 18 | // public var GIT_ATTR_CHECK_FILE_THEN_INDEX: Int32 { get } 19 | // public var GIT_ATTR_CHECK_INDEX_THEN_FILE: Int32 { get } 20 | // public var GIT_ATTR_CHECK_INDEX_ONLY: Int32 { get } 21 | // 22 | // /** 23 | // * Check attribute flags: controlling extended attribute behavior. 24 | // * 25 | // * Normally, attribute checks include looking in the /etc (or system 26 | // * equivalent) directory for a `gitattributes` file. Passing this 27 | // * flag will cause attribute checks to ignore that file. 28 | // * equivalent) directory for a `gitattributes` file. Passing the 29 | // * `GIT_ATTR_CHECK_NO_SYSTEM` flag will cause attribute checks to 30 | // * ignore that file. 31 | // * 32 | // * Passing the `GIT_ATTR_CHECK_INCLUDE_HEAD` flag will use attributes 33 | // * from a `.gitattributes` file in the repository at the HEAD revision. 34 | // */ 35 | // public var GIT_ATTR_CHECK_NO_SYSTEM: Int32 { get } 36 | // public var GIT_ATTR_CHECK_INCLUDE_HEAD: Int32 { get } 37 | 38 | public final class Attributes { 39 | public enum Value: Equatable, Hashable { 40 | case boolean(Bool) 41 | case string(String) 42 | } 43 | 44 | private weak var repository: Repository? 45 | 46 | init(_ repository: Repository) { 47 | self.repository = repository 48 | } 49 | 50 | public subscript(_ name: String) -> Value? { 51 | var pointer: UnsafePointer? 52 | return name.withCString { cString in 53 | guard case .success = result(of: { git_attr_get(&pointer, repository?.pointer, 0, ".gitattributes", cString) }) else { return nil } 54 | 55 | switch git_attr_value(pointer) { 56 | case GIT_ATTR_VALUE_TRUE: 57 | return .boolean(true) 58 | case GIT_ATTR_VALUE_FALSE: 59 | return .boolean(false) 60 | case GIT_ATTR_VALUE_STRING: 61 | return .string(String(validatingUTF8: pointer!) ?? "") 62 | default: 63 | return nil 64 | } 65 | } 66 | } 67 | } 68 | 69 | public var attributes: Attributes { 70 | return Attributes(self) 71 | } 72 | } 73 | 74 | extension Repository.Attributes.Value: ExpressibleByBooleanLiteral { 75 | public init(booleanLiteral value: Bool) { 76 | self = .boolean(value) 77 | } 78 | } 79 | 80 | extension Repository.Attributes.Value: ExpressibleByStringLiteral { 81 | public init(stringLiteral value: String) { 82 | self = .string(value) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/Git/Reference.swift: -------------------------------------------------------------------------------- 1 | import Clibgit2 2 | 3 | /** 4 | A branch, note, or tag. 5 | 6 | - SeeAlso: `Branch` 7 | - SeeAlso: `Note` 8 | - SeeAlso: `Tag` 9 | */ 10 | public class Reference/*: Identifiable */ { 11 | private(set) var pointer: OpaquePointer! 12 | 13 | private var managed: Bool = false 14 | 15 | required init(_ pointer: OpaquePointer) { 16 | self.pointer = pointer 17 | } 18 | 19 | deinit { 20 | guard managed else { return } 21 | git_reference_free(pointer) 22 | } 23 | 24 | // MARK: - 25 | 26 | /// Normalization options for reference lookup. 27 | public enum Format { 28 | /// No particular normalization. 29 | case normal 30 | 31 | /** 32 | Control whether one-level refnames are accepted 33 | (i.e., refnames that do not contain multiple `/`-separated components). 34 | 35 | Those are expected to be written only using 36 | uppercase letters and underscore (`FETCH_HEAD`, ...) 37 | */ 38 | case allowOneLevel 39 | 40 | /** 41 | Interpret the provided name as a reference pattern for a refspec 42 | (as used with remote repositories). 43 | 44 | If this option is enabled, 45 | the name is allowed to contain a single `*` () 46 | in place of a one full pathname component 47 | (e.g., `foo//bar` but not `foo/bar`). 48 | */ 49 | case refspecPattern 50 | 51 | /** 52 | Interpret the name as part of a refspec in shorthand form 53 | so the `ONELEVEL` naming rules aren't enforced 54 | and 'master' becomes a valid name. 55 | */ 56 | case refspecShorthand 57 | 58 | var rawValue: git_reference_format_t { 59 | switch self { 60 | case .normal: 61 | return GIT_REFERENCE_FORMAT_NORMAL 62 | case .allowOneLevel: 63 | return GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL 64 | case .refspecPattern: 65 | return GIT_REFERENCE_FORMAT_REFSPEC_PATTERN 66 | case .refspecShorthand: 67 | return GIT_REFERENCE_FORMAT_REFSPEC_SHORTHAND 68 | } 69 | } 70 | } 71 | 72 | public static func normalize(name: String, format: Format = .normal) throws -> String { 73 | let length = name.underestimatedCount * 2 74 | let cString = UnsafeMutablePointer.allocate(capacity: length) 75 | defer { cString.deallocate() } 76 | try attempt { git_reference_normalize_name(cString, length, name, format.rawValue.rawValue) } 77 | return String(validatingUTF8: cString)! 78 | } 79 | 80 | /// The reference name. 81 | public var name: String { 82 | return String(validatingUTF8: git_reference_name(pointer))! 83 | } 84 | 85 | /// The repository containing the reference. 86 | public var owner: Repository { 87 | return Repository(git_reference_owner(pointer)) 88 | } 89 | 90 | public var target: Object? { 91 | let id: Object.ID 92 | switch git_reference_type(pointer) { 93 | case GIT_REFERENCE_SYMBOLIC: 94 | var resolved: OpaquePointer? 95 | guard case .success = result(of: { git_reference_resolve(&resolved, pointer) }) else { return nil } 96 | defer { git_reference_free(resolved) } 97 | id = Object.ID(rawValue: git_reference_target(resolved).pointee) 98 | default: 99 | id = Object.ID(rawValue: git_reference_target(pointer).pointee) 100 | } 101 | 102 | return try? owner.lookup(type: Object.self, with: id) 103 | } 104 | } 105 | 106 | // MARK: - Equatable 107 | 108 | extension Reference: Equatable { 109 | public static func == (lhs: Reference, rhs: Reference) -> Bool { 110 | return git_reference_cmp(lhs.pointer, lhs.pointer) == 0 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Sources/Git/Signature.swift: -------------------------------------------------------------------------------- 1 | import Clibgit2 2 | import Foundation 3 | 4 | /** 5 | An action signature (e.g. for committers, taggers, etc). 6 | */ 7 | public struct Signature /*: internal RawRepresentable*/ { 8 | var rawValue: git_signature 9 | 10 | init(rawValue: git_signature) { 11 | self.rawValue = rawValue 12 | } 13 | 14 | public static func `default`(for repository: Repository) throws -> Signature { 15 | let pointer = UnsafeMutablePointer?>.allocate(capacity: 1) 16 | defer { pointer.deallocate() } 17 | try attempt { git_signature_default(pointer, repository.pointer) } 18 | 19 | return Signature(rawValue: pointer.pointee!.pointee) 20 | } 21 | 22 | // MARK: - 23 | 24 | /** 25 | Creates a signature with the specified name, email, time, and time zone. 26 | 27 | - Parameters: 28 | - name: The name of the signer. 29 | - email: The email of the signer. 30 | - time: The time at which the action occurred. 31 | - timeZone: The time's corresponding time zone. 32 | */ 33 | public init(name: String, 34 | email: String, 35 | time: Date = Date(), 36 | timeZone: TimeZone = TimeZone.current) throws 37 | { 38 | var pointer: UnsafeMutablePointer? 39 | let offset = Int32(timeZone.secondsFromGMT(for: time) / 60) 40 | let time = git_time_t(time.timeIntervalSince1970) 41 | try attempt { git_signature_new(&pointer, name, email, time, offset) } 42 | self.init(rawValue: pointer!.pointee) 43 | } 44 | 45 | /// The name of the signer. 46 | public var name: String { 47 | return String(validatingUTF8: rawValue.name)! 48 | } 49 | 50 | /// The email of the signer. 51 | public var email: String { 52 | String(validatingUTF8: rawValue.email)! 53 | } 54 | 55 | /// The time at which the action occurred. 56 | public var time: Date { 57 | return Date(timeIntervalSince1970: TimeInterval(rawValue.when.time)) 58 | } 59 | 60 | /// The time's corresponding time zone. 61 | public var timeZone: TimeZone? { 62 | return TimeZone(secondsFromGMT: 60 * Int(rawValue.when.offset)) 63 | } 64 | } 65 | 66 | // MARK: - Equatable 67 | 68 | extension Signature: Equatable { 69 | public static func == (lhs: Signature, rhs: Signature) -> Bool { 70 | return (lhs.name, lhs.email, lhs.time, lhs.timeZone) == (rhs.name, rhs.email, rhs.time, rhs.timeZone) 71 | } 72 | } 73 | 74 | // MARK: - Hashable 75 | 76 | extension Signature: Hashable { 77 | public func hash(into hasher: inout Hasher) { 78 | hasher.combine(name) 79 | hasher.combine(email) 80 | hasher.combine(rawValue.when.time) 81 | hasher.combine(rawValue.when.offset) 82 | } 83 | } 84 | 85 | // MARK: - CustomStringConvertible 86 | 87 | extension Signature: CustomStringConvertible { 88 | public var description: String { 89 | return "\(name) <\(email)>" 90 | } 91 | } 92 | 93 | // MARK: - Codable 94 | 95 | extension Signature: Codable { 96 | private enum CodingKeys: String, CodingKey { 97 | case name 98 | case email 99 | case time 100 | case timeZone 101 | } 102 | 103 | public init(from decoder: Decoder) throws { 104 | let container = try decoder.container(keyedBy: CodingKeys.self) 105 | let name = try container.decode(String.self, forKey: .name) 106 | let email = try container.decode(String.self, forKey: .email) 107 | let time = try container.decode(Date.self, forKey: .time) 108 | let timeZone = try container.decode(TimeZone.self, forKey: .timeZone) 109 | 110 | try self.init(name: name, email: email, time: time, timeZone: timeZone) 111 | } 112 | 113 | public func encode(to encoder: Encoder) throws { 114 | var container = encoder.container(keyedBy: CodingKeys.self) 115 | try container.encode(name, forKey: .name) 116 | try container.encode(email, forKey: .email) 117 | try container.encode(time, forKey: .time) 118 | try container.encode(timeZone, forKey: .timeZone) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Sources/Git/Repository/Clone.swift: -------------------------------------------------------------------------------- 1 | import Clibgit2 2 | import Foundation 3 | 4 | extension Repository { 5 | public enum Clone { 6 | public enum Local /* : internal RawRepresentable */ { 7 | /** 8 | * Auto-detect (default), libgit2 will bypass the git-aware 9 | * transport for local paths, but use a normal fetch for 10 | * `file://` urls. 11 | */ 12 | case automatic 13 | 14 | /// Bypass the git-aware transport even for a `file://` url. 15 | case yes(useHardLinks: Bool = true) 16 | 17 | /// Do no bypass the git-aware transport 18 | case no 19 | 20 | init(rawValue: git_clone_local_t) { 21 | switch rawValue { 22 | case GIT_CLONE_LOCAL: 23 | self = .yes(useHardLinks: true) 24 | case GIT_CLONE_LOCAL_NO_LINKS: 25 | self = .yes(useHardLinks: false) 26 | case GIT_CLONE_NO_LOCAL: 27 | self = .no 28 | default: 29 | self = .automatic 30 | } 31 | } 32 | 33 | public var rawValue: git_clone_local_t { 34 | switch self { 35 | case .automatic: 36 | return GIT_CLONE_LOCAL_AUTO 37 | case .yes(useHardLinks: true): 38 | return GIT_CLONE_LOCAL 39 | case .yes(useHardLinks: false): 40 | return GIT_CLONE_LOCAL_NO_LINKS 41 | case .no: 42 | return GIT_CLONE_NO_LOCAL 43 | } 44 | } 45 | } 46 | 47 | public struct Configuration { 48 | var rawValue: git_clone_options 49 | 50 | public static var `default` = try! Configuration() 51 | 52 | init() throws { 53 | let pointer = UnsafeMutablePointer.allocate(capacity: 1) 54 | defer { pointer.deallocate() } 55 | try attempt { git_clone_options_init(pointer, numericCast(GIT_CLONE_OPTIONS_VERSION)) } 56 | rawValue = pointer.pointee 57 | } 58 | 59 | init(rawValue: git_clone_options) { 60 | self.rawValue = rawValue 61 | } 62 | 63 | public var checkoutConfiguration: Repository.Checkout.Configuration { 64 | get { 65 | Repository.Checkout.Configuration(rawValue: rawValue.checkout_opts) 66 | } 67 | 68 | set { 69 | rawValue.checkout_opts = newValue.rawValue 70 | } 71 | } 72 | 73 | public var fetchConfiguration: Remote.Fetch.Configuration { 74 | get { 75 | Remote.Fetch.Configuration(rawValue: rawValue.fetch_opts) 76 | } 77 | 78 | set { 79 | rawValue.fetch_opts = newValue.rawValue 80 | } 81 | } 82 | 83 | /// Set to zero (false) to create a standard repo, or non-zero for a bare repo 84 | public var bare: Bool { 85 | get { 86 | rawValue.bare != 0 87 | } 88 | 89 | set { 90 | rawValue.bare = newValue ? 1 : 0 91 | } 92 | } 93 | 94 | /// Whether to use a fetch or copy the object database. 95 | public var local: Local { 96 | get { 97 | Local(rawValue: rawValue.local) 98 | } 99 | 100 | set { 101 | rawValue.local = newValue.rawValue 102 | } 103 | } 104 | 105 | /// The name of the branch to checkout. NULL means use the remote's default branch. 106 | public var checkoutBranch: String? { 107 | get { 108 | guard let cString = rawValue.checkout_branch else { return nil } 109 | return String(validatingUTF8: cString) 110 | } 111 | 112 | set { 113 | newValue?.withCString({ cString in rawValue.checkout_branch = cString }) 114 | } 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Sources/Git/Remote.swift: -------------------------------------------------------------------------------- 1 | import Clibgit2 2 | import Foundation 3 | 4 | public class Remote /*: internal RawRepresentable*/ { 5 | private var rawValue: OpaquePointer! 6 | 7 | var managed: Bool = false 8 | 9 | init(rawValue: OpaquePointer) { 10 | self.rawValue = rawValue 11 | } 12 | 13 | deinit { 14 | guard managed else { return } 15 | git_remote_free(rawValue) 16 | } 17 | 18 | // MARK: - 19 | 20 | /** 21 | Creates a detatched remote with the specified name and url. 22 | 23 | - Parameters: 24 | - name: The name of the remote, if any. 25 | - url: The url of the remote. 26 | */ 27 | public convenience init(name: String? = nil, url: URL) throws { 28 | fatalError() // TODO 29 | } 30 | } 31 | 32 | // MARK: - 33 | 34 | extension Remote { 35 | public enum Fetch { 36 | public enum TagFollowing { 37 | /// Use the setting from the configuration. 38 | case `default` 39 | 40 | /// Ask the server for tags pointing to objects we're already downloading. 41 | case automatic 42 | 43 | /// Ask for the all the tags. 44 | case all 45 | } 46 | 47 | public struct Configuration { 48 | var rawValue: git_fetch_options 49 | 50 | public static var `default` = try! Configuration() 51 | 52 | init() throws { 53 | let pointer = UnsafeMutablePointer.allocate(capacity: 1) 54 | defer { pointer.deallocate() } 55 | try attempt { git_fetch_options_init(pointer, numericCast(GIT_FETCH_OPTIONS_VERSION)) } 56 | rawValue = pointer.pointee 57 | } 58 | 59 | init(rawValue: git_fetch_options) { 60 | self.rawValue = rawValue 61 | } 62 | 63 | /// Whether to write the results to FETCH_HEAD. Defaults to on. Leave this default in order to behave like git. 64 | public var updateFetchHead: Bool { 65 | get { 66 | rawValue.update_fetchhead != 0 67 | } 68 | 69 | set { 70 | rawValue.update_fetchhead = newValue ? 1 : 0 71 | } 72 | } 73 | 74 | public var prune: Bool? { 75 | get { 76 | switch rawValue.prune { 77 | case GIT_FETCH_PRUNE: return true 78 | case GIT_FETCH_NO_PRUNE: return false 79 | default: 80 | return nil 81 | } 82 | } 83 | 84 | set { 85 | switch newValue { 86 | case true?: 87 | rawValue.prune = GIT_FETCH_PRUNE 88 | case false?: 89 | rawValue.prune = GIT_FETCH_NO_PRUNE 90 | case nil: 91 | rawValue.prune = GIT_FETCH_PRUNE_UNSPECIFIED 92 | } 93 | } 94 | } 95 | 96 | public var tagFollowing: TagFollowing? { 97 | get { 98 | return TagFollowing?(rawValue: rawValue.download_tags) 99 | } 100 | 101 | set { 102 | rawValue.download_tags = newValue.rawValue 103 | } 104 | } 105 | 106 | /// Extra headers for this fetch operation 107 | public var customHeaders: [String] { 108 | get { 109 | Array(rawValue.custom_headers) 110 | } 111 | 112 | // TODO: setter 113 | } 114 | } 115 | } 116 | } 117 | 118 | extension Optional /*: internal RawRepresentable */ where Wrapped == Remote.Fetch.TagFollowing { 119 | init(rawValue: git_remote_autotag_option_t) { 120 | switch rawValue { 121 | case GIT_REMOTE_DOWNLOAD_TAGS_UNSPECIFIED: 122 | self = .default 123 | case GIT_REMOTE_DOWNLOAD_TAGS_AUTO: 124 | self = .automatic 125 | case GIT_REMOTE_DOWNLOAD_TAGS_ALL: 126 | self = .all 127 | default: 128 | self = .none 129 | } 130 | } 131 | 132 | var rawValue: git_remote_autotag_option_t { 133 | switch self { 134 | case .default?: 135 | return GIT_REMOTE_DOWNLOAD_TAGS_UNSPECIFIED 136 | case .automatic?: 137 | return GIT_REMOTE_DOWNLOAD_TAGS_AUTO 138 | case .all?: 139 | return GIT_REMOTE_DOWNLOAD_TAGS_ALL 140 | default: 141 | return GIT_REMOTE_DOWNLOAD_TAGS_NONE 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Tests/GitTests/GitTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Git 3 | import Clibgit2 4 | import Foundation 5 | 6 | final class GitTests: XCTestCase { 7 | func testCloneAndReadRepository() throws { 8 | let remoteURL = URL(string: "https://github.com/SwiftDocOrg/StringLocationConverter.git")! 9 | 10 | let localURL = URL(fileURLWithPath: temporaryURL().path) 11 | try FileManager.default.createDirectory(at: localURL, withIntermediateDirectories: true) 12 | 13 | try Repository.clone(from: remoteURL, to: localURL) 14 | let repository = try Repository.open(at: localURL) 15 | 16 | let directoryURL = repository.workingDirectory 17 | XCTAssertNotNil(directoryURL) 18 | 19 | do { 20 | let head = repository.head 21 | XCTAssertEqual(head?.branch?.name, "refs/heads/master") 22 | XCTAssertEqual(head?.attached, true) 23 | 24 | let tree = head?.branch?.commit?.tree 25 | XCTAssertEqual(tree?.count, 6) 26 | 27 | XCTAssert(tree?["README.md"]?.object is Blob) 28 | XCTAssert(tree?["Sources"]?.object is Tree) 29 | 30 | do { 31 | let blob = tree?["README.md"]?.object as? Blob 32 | XCTAssertNotNil(blob) 33 | 34 | let string = String(data: blob!.data, encoding: .utf8) 35 | XCTAssert(string!.starts(with: "# StringLocationConverter")) 36 | } 37 | 38 | do { 39 | let subtree = tree?["Sources"]?.object as? Tree 40 | XCTAssertNotNil(subtree) 41 | 42 | XCTAssertEqual(subtree?.count, 1) 43 | XCTAssertNotNil(subtree?["StringLocationConverter"]) 44 | XCTAssert(subtree?["StringLocationConverter"]?.object is Tree) 45 | } 46 | } 47 | 48 | do { 49 | let master = try repository.branch(named: "refs/heads/master") 50 | XCTAssertEqual(master?.shortName, "master") 51 | XCTAssertEqual(master?.commit?.message?.trimmingCharacters(in: .whitespacesAndNewlines), "Initial commit") 52 | XCTAssertEqual(master?.commit?.id.description, "6cf6579c191e20a5a77a7e3176d37a8d654c9fc4") 53 | XCTAssertEqual(master?.commit?.author.name, "Mattt") 54 | } 55 | 56 | do { 57 | let index = repository.index 58 | let blobs = index?.compactMap { $0.blob } 59 | XCTAssertEqual(blobs?.count, 9) 60 | 61 | let revisions = try repository.revisions { walker in 62 | try walker.pushHead() 63 | } 64 | 65 | XCTAssertEqual(Array(revisions.compactMap { $0.message }), ["Initial commit\n"]) 66 | 67 | let entry = index?["Sources/StringLocationConverter/Location.swift"] 68 | XCTAssertNotNil(entry) 69 | 70 | let source = String(data: entry!.blob!.data, encoding: .utf8)! 71 | XCTAssert(source.hasPrefix("/// A location within a string when displayed with newlines.")) 72 | } 73 | } 74 | 75 | func testCreateAndCommitToRepository() throws { 76 | let localURL = URL(fileURLWithPath: temporaryURL().path) 77 | try FileManager.default.createDirectory(at: localURL, withIntermediateDirectories: true) 78 | let repository = try Repository.create(at: localURL) 79 | 80 | try """ 81 | Hello, world! 82 | """.write(toFile: localURL.appendingPathComponent("hello.txt").path, atomically: true, encoding: .utf8) 83 | 84 | try repository.index?.add(paths: ["hello.txt"]) 85 | 86 | let signature = try Signature(name: "Mona Lisa Octocat", email: "mona@github.com") 87 | let commit = try repository.createCommit(message: "Initial commit", author: signature, committer: signature) 88 | 89 | XCTAssertEqual(repository.head?.commit, commit) 90 | XCTAssertEqual(commit.message, "Initial commit") 91 | 92 | let tree = commit.tree 93 | XCTAssertNotNil(tree) 94 | XCTAssertEqual(tree?.count, 1) 95 | XCTAssertNotNil(tree?["hello.txt"]) 96 | 97 | let blob = tree?["hello.txt"]?.object as? Blob 98 | XCTAssertNotNil(blob) 99 | XCTAssertEqual(String(data: blob!.data, encoding: .utf8), "Hello, world!") 100 | 101 | let note = try commit.addNote(#"{"test": true }"#, author: signature, committer: signature) 102 | XCTAssertNotNil(note?.message, #"{"test": true }"#) 103 | 104 | try repository.createLightweightTag(named: "0.0.1", target: commit) 105 | let names = try repository.tagNames() 106 | XCTAssert(names.contains("0.0.1")) 107 | 108 | XCTAssertEqual((repository["0.0.1"]?.target as? Commit)?.message, "Initial commit") 109 | XCTAssertEqual((repository["master"]?.target as? Commit)?.message, "Initial commit") 110 | XCTAssertEqual((repository["HEAD"]?.target as? Commit)?.message, "Initial commit") 111 | XCTAssertEqual((repository["HEAD"]?.target as? Commit)?.note?.message, #"{"test": true }"#) 112 | } 113 | } 114 | 115 | // MARK: - 116 | 117 | fileprivate func temporaryURL() -> URL { 118 | let globallyUniqueString = ProcessInfo.processInfo.globallyUniqueString 119 | let path = "\(NSTemporaryDirectory())\(globallyUniqueString)" 120 | return URL(fileURLWithPath: path) 121 | } 122 | -------------------------------------------------------------------------------- /Sources/Git/RevisionWalker.swift: -------------------------------------------------------------------------------- 1 | import Clibgit2 2 | 3 | /** 4 | Options for the order of the sequence returned by 5 | `Repository.revisions(with:)`. 6 | 7 | - SeeAlso: `Repository.revisions(with:)` 8 | */ 9 | public struct RevisionSortingOptions: OptionSet { 10 | public var rawValue: UInt32 11 | 12 | public init(rawValue: UInt32) { 13 | self.rawValue = rawValue 14 | } 15 | 16 | /** 17 | Sort the repository contents by commit time. 18 | */ 19 | public static var time = RevisionSortingOptions(rawValue: GIT_SORT_TIME.rawValue) 20 | 21 | /** 22 | Sort the repository contents in topological order 23 | (no parents before all of its children are shown). 24 | */ 25 | public static var topological = RevisionSortingOptions(rawValue: GIT_SORT_TOPOLOGICAL.rawValue) 26 | 27 | /** 28 | Iterate through the repository contents in reverse order. 29 | */ 30 | public static var reverse = RevisionSortingOptions(rawValue: GIT_SORT_REVERSE.rawValue) 31 | } 32 | 33 | /** 34 | An interface for configuring which revisions are returned by 35 | `Repository.revisions(with:)`. 36 | 37 | - SeeAlso: `Repository.revisions(with:)` 38 | */ 39 | public protocol RevisionWalker { 40 | /// Push the repository's `HEAD`. 41 | func pushHead() throws 42 | 43 | /// Push matching references. 44 | func pushGlob(_ glob: String) throws 45 | 46 | /// Push a range of references. 47 | func pushRange(_ range: String) throws 48 | 49 | /// Push a reference by name. 50 | func pushReference(named name: String) throws 51 | 52 | /// Hide the repository's `HEAD`. 53 | func hideHead() throws 54 | 55 | /// Hide matching references. 56 | func hideGlob(_ glob: String) throws 57 | 58 | /// Hide a commit by ID. 59 | func hideCommit(with id: Commit.ID) throws 60 | 61 | /// Hide a reference by name. 62 | func hideReference(named name: String) throws 63 | 64 | /// Sort revisions with the provided options. 65 | func sort(with options: RevisionSortingOptions) throws 66 | 67 | /** 68 | Simplify the history such that 69 | no parents other than the first for each commit will be enqueued. 70 | */ 71 | func simplifyFirstParent() throws 72 | } 73 | 74 | extension Repository { 75 | final class Revisions: Sequence, IteratorProtocol, RevisionWalker { 76 | private(set) var pointer: OpaquePointer! 77 | 78 | init(_ repository: Repository) throws { 79 | try attempt { git_revwalk_new(&pointer, repository.pointer) } 80 | } 81 | 82 | deinit { 83 | git_revwalk_free(pointer) 84 | } 85 | 86 | var repository: Repository { 87 | return Repository(git_revwalk_repository(pointer)) 88 | } 89 | 90 | // MARK: - Sequence 91 | 92 | func next() -> Commit? { 93 | let pointer = UnsafeMutablePointer.allocate(capacity: 1) 94 | defer { pointer.deallocate() } 95 | guard case .success = result(of: { git_revwalk_next(pointer, self.pointer) }) else { return nil } 96 | let id = Object.ID(rawValue: pointer.pointee) 97 | return try? repository.lookup(type: Commit.self, with: id) 98 | } 99 | 100 | // MARK: - RevisionWalker 101 | 102 | func pushHead() throws { 103 | try attempt { git_revwalk_push_head(pointer) } 104 | } 105 | 106 | func pushGlob(_ glob: String) throws { 107 | try glob.withCString { cString in 108 | try attempt { git_revwalk_push_glob(pointer, cString) } 109 | } 110 | } 111 | 112 | func pushRange(_ range: String) throws { 113 | try range.withCString { cString in 114 | try attempt { git_revwalk_push_range(pointer, cString) } 115 | } 116 | } 117 | 118 | func pushReference(named name: String) throws { 119 | try name.withCString { cString in 120 | try attempt { git_revwalk_push_ref(pointer, cString) } 121 | } 122 | } 123 | 124 | func hideGlob(_ glob: String) throws { 125 | try glob.withCString { cString in 126 | try attempt { git_revwalk_hide_glob(pointer, cString) } 127 | } 128 | } 129 | 130 | func hideHead() throws { 131 | try attempt { git_revwalk_hide_head(pointer) } 132 | } 133 | 134 | func hideCommit(with id: Commit.ID) throws { 135 | var oid = id.rawValue 136 | try attempt { git_revwalk_hide(pointer, &oid) } 137 | } 138 | 139 | func hideReference(named name: String) throws { 140 | try name.withCString { cString in 141 | try attempt { git_revwalk_hide_ref(pointer, cString) } 142 | } 143 | } 144 | 145 | func sort(with options: RevisionSortingOptions) throws { 146 | try attempt { git_revwalk_sorting(pointer, options.rawValue) } 147 | } 148 | 149 | func simplifyFirstParent() throws { 150 | try attempt { git_revwalk_simplify_first_parent(pointer) } 151 | } 152 | 153 | func reset() throws { 154 | try attempt { git_revwalk_reset(pointer) } 155 | } 156 | } 157 | 158 | /** 159 | Returns a sequence of revisions according to the specified configuration. 160 | 161 | - Parameters: 162 | - configuration: A closure whose argument can be modified to 163 | change which revisions are returned by the sequence, 164 | and the order in which they appear. 165 | - Throws: Any error that occured during configuration. 166 | - Returns: A sequence of revisions. 167 | */ 168 | public func revisions(with configuration: (RevisionWalker) throws -> Void ) throws -> AnySequence { 169 | let revisions = try Revisions(self) 170 | try configuration(revisions) 171 | return AnySequence(revisions) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /Sources/Git/Object.swift: -------------------------------------------------------------------------------- 1 | import Clibgit2 2 | import struct Foundation.Data 3 | 4 | /// 5 | 6 | /** 7 | A blob, commit, tree, or tag annotation. 8 | 9 | - SeeAlso: `Blob` 10 | - SeeAlso: `Commit` 11 | - SeeAlso: `Tree` 12 | - SeeAlso: `Tag.Revision` 13 | */ 14 | public class Object { 15 | class var type: git_object_t { return GIT_OBJECT_ANY } 16 | 17 | private(set) var pointer: OpaquePointer! 18 | 19 | private var managed: Bool = false 20 | 21 | /// The repository containing the object. 22 | public var owner: Repository { 23 | return Repository(git_object_owner(pointer)) 24 | } 25 | 26 | required init(_ pointer: OpaquePointer) { 27 | self.pointer = pointer 28 | assert(Swift.type(of: self) == Object.self || git_object_type(pointer) == Swift.type(of: self).type) 29 | } 30 | 31 | deinit { 32 | if managed { 33 | git_repository_free(pointer) 34 | } 35 | } 36 | 37 | class func type(of object: Object) -> Object.Type? { 38 | return Object.type(of: object.pointer) 39 | } 40 | 41 | class func type(of pointer: OpaquePointer?) -> Object.Type? { 42 | guard let pointer = pointer else { return nil } 43 | return Object.type(of: git_object_type(pointer)) 44 | } 45 | 46 | class func type(of value: git_object_t?) -> Object.Type? { 47 | switch value { 48 | case GIT_OBJECT_ANY?: 49 | return Object.self 50 | case GIT_OBJECT_COMMIT?: 51 | return Commit.self 52 | case GIT_OBJECT_TREE?: 53 | return Tree.self 54 | case GIT_OBJECT_BLOB?: 55 | return Blob.self 56 | case GIT_OBJECT_TAG?: 57 | return Tag.Annotation.self 58 | // TODO: 59 | // case GIT_OBJECT_OFS_DELTA, 60 | // GIT_OBJECT_REF_DELTA: 61 | // return Delta.self 62 | default: 63 | return nil 64 | } 65 | } 66 | 67 | /// The object's ID. 68 | public var id: ID { 69 | return ID(rawValue: git_object_id(pointer).pointee) 70 | } 71 | 72 | /// The note attached to the object, if any. 73 | public var note: Note? { 74 | var pointer: OpaquePointer? 75 | let owner = git_commit_owner(self.pointer) 76 | var oid = id.rawValue 77 | guard case .success = result(of: { git_note_read(&pointer, owner, nil, &oid) }) else { return nil } 78 | 79 | return Note(pointer!) 80 | } 81 | 82 | @discardableResult 83 | public func addNote(_ message: String, author: Signature? = nil, committer: Signature? = nil, force: Bool = false) throws -> Note? { 84 | let repository = owner 85 | 86 | var committer = (try committer ?? author ?? Signature.default(for: repository)).rawValue 87 | var author = (try author ?? Signature.default(for: repository)).rawValue 88 | 89 | // TODO determine parent Note commit 90 | 91 | var objectOID = id.rawValue 92 | try attempt { git_note_create(nil, repository.pointer, nil, &author, &committer, &objectOID, message, force ? 1 : 0) } 93 | 94 | return self.note 95 | } 96 | } 97 | 98 | 99 | // MARK: - Equatable 100 | 101 | extension Object: Equatable { 102 | public static func == (lhs: Object, rhs: Object) -> Bool { 103 | return lhs.id == rhs.id 104 | } 105 | } 106 | 107 | // MARK: - Hashable 108 | 109 | extension Object: Hashable { 110 | public func hash(into hasher: inout Hasher) { 111 | hasher.combine(id) 112 | } 113 | } 114 | 115 | 116 | // MARK: - 117 | 118 | extension Object { 119 | /// An object ID. 120 | public struct ID: Equatable, Hashable, CustomStringConvertible, ExpressibleByStringLiteral /*: internal RawRepresentable*/ { 121 | let rawValue: git_oid 122 | 123 | init(rawValue: git_oid) { 124 | self.rawValue = rawValue 125 | } 126 | 127 | public init() { 128 | self.init(rawValue: git_oid()) 129 | } 130 | 131 | public init(_ body: (UnsafeMutablePointer) throws -> T) rethrows { 132 | var pointer = git_oid() 133 | _ = try body(&pointer) 134 | self.init(rawValue: pointer) 135 | } 136 | 137 | /// Creates an Object ID from a string. 138 | public init(string: String) throws { 139 | precondition(string.lengthOfBytes(using: .ascii) <= 40) 140 | let pointer = UnsafeMutablePointer.allocate(capacity: 1) 141 | defer { pointer.deallocate() } 142 | try attempt { git_oid_fromstr(pointer, string) } 143 | rawValue = pointer.pointee 144 | } 145 | 146 | /// Creates an Object ID from a byte array. 147 | public init(bytes: [UInt8]) throws { 148 | precondition(bytes.count <= 40) 149 | let pointer = UnsafeMutablePointer.allocate(capacity: 1) 150 | defer { pointer.deallocate() } 151 | try attempt { git_oid_fromraw(pointer, bytes) } 152 | rawValue = pointer.pointee 153 | } 154 | 155 | /// Creates an Object ID from data. 156 | public init(data: Data) throws { 157 | precondition(data.underestimatedCount <= 40) 158 | let bytes = data.withUnsafeBytes { $0.load(as: [UInt8].self) } 159 | try self.init(bytes: bytes) 160 | } 161 | 162 | // MARK: - Equatable 163 | 164 | public static func == (lhs: Object.ID, rhs: Object.ID) -> Bool { 165 | var loid = lhs.rawValue, roid = rhs.rawValue 166 | return git_oid_cmp(&loid, &roid) == 0 167 | } 168 | 169 | // MARK: - Hashable 170 | 171 | public func hash(into hasher: inout Hasher) { 172 | withUnsafeBytes(of: rawValue.id) { 173 | hasher.combine(bytes: $0) 174 | } 175 | } 176 | 177 | // MARK: - CustomStringConvertible 178 | 179 | public var description: String { 180 | let length = Int(GIT_OID_HEXSZ) 181 | let string = UnsafeMutablePointer.allocate(capacity: length) 182 | var oid = self.rawValue 183 | git_oid_fmt(string, &oid) 184 | 185 | return String(bytesNoCopy: string, length: length, encoding: .ascii, freeWhenDone: true)! 186 | } 187 | 188 | // MARK: - ExpressibleByStringLiteral 189 | 190 | public init(stringLiteral value: StringLiteralType) { 191 | try! self.init(string: value) 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /Sources/Git/Repository/Index.swift: -------------------------------------------------------------------------------- 1 | import Clibgit2 2 | import Foundation 3 | 4 | extension Repository { 5 | /// A repository index. 6 | public final class Index { 7 | private(set) var pointer: OpaquePointer! 8 | 9 | var managed: Bool = false 10 | 11 | init(_ pointer: OpaquePointer) { 12 | self.pointer = pointer 13 | } 14 | 15 | deinit { 16 | guard managed else { return } 17 | git_index_free(pointer) 18 | } 19 | 20 | // MARK: - 21 | 22 | /** 23 | The index on-disk version. 24 | 25 | Valid return values are 2, 3, or 4. 26 | If 3 is returned, an index with version 2 may be written instead, 27 | if the extension data in version 3 is not necessary. 28 | */ 29 | public var version: Int { 30 | return Int(git_index_version(pointer)) 31 | } 32 | 33 | /// The repository for the index. 34 | public var owner: Repository { 35 | return Repository(git_index_owner(pointer)) 36 | } 37 | 38 | /// The file path to the repository index file. 39 | public var path: String! { 40 | return String(validatingUTF8: git_index_path(pointer)) 41 | } 42 | 43 | /** 44 | Update the contents of an existing index object in memory 45 | by reading from disk. 46 | 47 | - Important: If there are changes on disk, 48 | unwritten in-memory changes are discarded. 49 | 50 | - Parameters: 51 | - force: If true, this performs a "hard" read 52 | that discards in-memory changes 53 | and always reloads the on-disk index data. 54 | If there is no on-disk version, 55 | the index will be cleared. 56 | If false, 57 | this does a "soft" read that reloads the index data from disk 58 | only if it has changed since the last time it was loaded. 59 | Purely in-memory index data will be untouched. 60 | */ 61 | public func reload(force: Bool) throws { 62 | try attempt { git_index_read(pointer, force ? 1 : 0)} 63 | } 64 | 65 | public func add(path: String, force: Bool = false) throws { 66 | try path.withCString { cString in 67 | try attempt { git_index_add_bypath(pointer, cString) } 68 | } 69 | } 70 | 71 | // TODO: Add dry-run option 72 | public func add(paths: [String], update: Bool = false, force: Bool = false, disableGlobExpansion: Bool = false) throws { 73 | let options = (force ? GIT_INDEX_ADD_FORCE.rawValue : 0) | 74 | (disableGlobExpansion ? GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH.rawValue : 0) 75 | 76 | try paths.withGitStringArray { array in 77 | try withUnsafePointer(to: array) { paths in 78 | if update { 79 | try attempt { git_index_update_all(pointer, paths, nil, nil) } 80 | } else { 81 | try attempt { git_index_add_all(pointer, paths, options, nil, nil) } 82 | } 83 | } 84 | } 85 | 86 | try attempt { git_index_write(pointer) } 87 | } 88 | } 89 | } 90 | 91 | extension Repository.Index { 92 | public enum Stage /* : internal RawRepresentable */ { 93 | case normal 94 | case ancestor 95 | case ours 96 | case theirs 97 | 98 | init(rawValue: git_index_stage_t) { 99 | switch rawValue { 100 | case GIT_INDEX_STAGE_ANCESTOR: 101 | self = .ancestor 102 | case GIT_INDEX_STAGE_OURS: 103 | self = .ours 104 | case GIT_INDEX_STAGE_THEIRS: 105 | self = .theirs 106 | default: 107 | self = .normal 108 | } 109 | } 110 | 111 | var rawValue: git_index_stage_t { 112 | switch self { 113 | case .normal: 114 | return GIT_INDEX_STAGE_NORMAL 115 | case .ancestor: 116 | return GIT_INDEX_STAGE_ANCESTOR 117 | case .ours: 118 | return GIT_INDEX_STAGE_OURS 119 | case .theirs: 120 | return GIT_INDEX_STAGE_THEIRS 121 | } 122 | } 123 | } 124 | } 125 | 126 | extension Repository.Index { 127 | /// An entry in the index. 128 | public final class Entry: Equatable, Comparable, Hashable { 129 | weak var index: Repository.Index? 130 | private(set) var rawValue: git_index_entry 131 | 132 | required init(rawValue: git_index_entry) { 133 | self.rawValue = rawValue 134 | } 135 | 136 | convenience init?(in index: Repository.Index, at n: Int) { 137 | let pointer = git_index_get_byindex(index.pointer, n) 138 | guard let rawValue = pointer?.pointee else { return nil } 139 | 140 | self.init(rawValue: rawValue) 141 | self.index = index 142 | } 143 | 144 | convenience init?(in index: Repository.Index, at path: String, stage: Stage) { 145 | let pointer = path.withCString { cString in 146 | git_index_get_bypath(index.pointer, cString, stage.rawValue.rawValue) 147 | } 148 | guard let rawValue = pointer?.pointee else { return nil } 149 | 150 | self.init(rawValue: rawValue) 151 | self.index = index 152 | } 153 | 154 | /// The file path of the index entry. 155 | public var path: String { 156 | return String(validatingUTF8: rawValue.path)! 157 | } 158 | 159 | /// The size of the index entry. 160 | public var fileSize: Int { 161 | return Int(rawValue.file_size) 162 | } 163 | 164 | /// The creation time of the index entry. 165 | public var creationTime: Date { 166 | return Date(timeIntervalSince1970: TimeInterval(rawValue.ctime.seconds)) 167 | } 168 | 169 | /// The modification time of the index entry 170 | public var modificationTime: Date { 171 | return Date(timeIntervalSince1970: TimeInterval(rawValue.mtime.seconds)) 172 | } 173 | 174 | /// The blob object for the index entry, if any. 175 | public var blob: Blob? { 176 | let id = Object.ID(rawValue: rawValue.id) 177 | return try? index?.owner.lookup(type: Blob.self, with: id) 178 | } 179 | 180 | public var isConflict: Bool { 181 | git_index_entry_is_conflict(&rawValue) != 0 182 | } 183 | 184 | public var stage: Stage { 185 | Stage(rawValue: git_index_stage_t(git_index_entry_stage(&rawValue))) 186 | } 187 | 188 | // MARK: - Equatable 189 | 190 | public static func == (lhs: Repository.Index.Entry, rhs: Repository.Index.Entry) -> Bool { 191 | var loid = lhs.rawValue.id, roid = rhs.rawValue.id 192 | return git_oid_cmp(&loid, &roid) == 0 193 | } 194 | 195 | // MARK: - Comparable 196 | 197 | public static func < (lhs: Repository.Index.Entry, rhs: Repository.Index.Entry) -> Bool { 198 | return lhs.path < rhs.path 199 | } 200 | 201 | // MARK: - Hashable 202 | 203 | public func hash(into hasher: inout Hasher) { 204 | hasher.combine(rawValue.uid) 205 | } 206 | } 207 | 208 | final class Entries: Sequence, IteratorProtocol { 209 | private weak var index: Repository.Index? 210 | private(set) var pointer: OpaquePointer! 211 | 212 | init(_ index: Repository.Index) throws { 213 | try attempt { git_index_iterator_new(&pointer, index.pointer) } 214 | self.index = index 215 | } 216 | 217 | deinit { 218 | git_index_iterator_free(pointer) 219 | } 220 | 221 | var underestimatedCount: Int { 222 | guard let index = index else { return 0 } 223 | return git_index_entrycount(index.pointer) 224 | } 225 | 226 | // MARK: - Sequence 227 | 228 | func next() -> Entry? { 229 | do { 230 | var pointer: UnsafePointer? 231 | try attempt { git_index_iterator_next(&pointer, self.pointer) } 232 | let entry = Entry(rawValue: pointer!.pointee) 233 | entry.index = index 234 | return entry 235 | } catch { 236 | return nil 237 | } 238 | } 239 | } 240 | 241 | 242 | /// Returns a sequence of entries in the index. 243 | public var entries: AnySequence { 244 | guard let entries = try? Entries(self) else { return AnySequence(EmptyCollection()) } 245 | return AnySequence(entries) 246 | } 247 | } 248 | 249 | // MARK: - RandomAccessCollection 250 | 251 | extension Repository.Index: RandomAccessCollection { 252 | public typealias Element = Entry 253 | 254 | public var startIndex: Int { 0 } 255 | public var endIndex: Int { git_index_entrycount(pointer) } 256 | 257 | public subscript(_ index: Int) -> Entry { 258 | precondition(indices.contains(index)) 259 | return Entry(in: self, at: index)! 260 | } 261 | 262 | public subscript(_ path: String, stage: Stage = .normal) -> Entry? { 263 | return Entry(in: self, at: path, stage: stage) 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /Sources/Git/Repository.swift: -------------------------------------------------------------------------------- 1 | import Clibgit2 2 | import Foundation 3 | 4 | public final class Repository { 5 | private(set) var pointer: OpaquePointer! 6 | private var managed: Bool = false 7 | 8 | init(_ pointer: OpaquePointer) { 9 | self.pointer = pointer 10 | } 11 | 12 | deinit { 13 | guard managed else { return } 14 | git_repository_free(pointer) 15 | } 16 | 17 | // MARK: - 18 | 19 | public class func open(at url: URL) throws -> Repository { 20 | var pointer: OpaquePointer? 21 | try attempt { git_repository_open(&pointer, url.path) } 22 | return Repository(pointer!) 23 | } 24 | 25 | public class func create(at url: URL, bare: Bool = false) throws -> Repository { 26 | var pointer: OpaquePointer? 27 | try attempt { git_repository_init(&pointer, url.path, bare ? 1 : 0) } 28 | return Repository(pointer!) 29 | } 30 | 31 | public class func discover(at url: URL, acrossFileSystems: Bool = true, stoppingAt ceilingDirectories: [String] = []) throws -> Repository { 32 | var buffer = git_buf() 33 | defer { git_buf_free(&buffer) } 34 | 35 | try url.withUnsafeFileSystemRepresentation { path in 36 | try attempt { git_repository_discover(&buffer, path, acrossFileSystems ? 1 : 0, ceilingDirectories.joined(separator: pathListSeparator).cString(using: .utf8)) } 37 | } 38 | 39 | let discoveredURL = URL(fileURLWithPath: String(cString: buffer.ptr)) 40 | 41 | return try Repository.open(at: discoveredURL) 42 | } 43 | 44 | @discardableResult 45 | public static func clone(from remoteURL: URL, 46 | to localURL: URL, 47 | configuration: Clone.Configuration = .default) throws -> Repository { 48 | var pointer: OpaquePointer? 49 | 50 | var options = configuration.rawValue 51 | let remoteURLString = remoteURL.isFileURL ? remoteURL.path : remoteURL.absoluteString 52 | try localURL.withUnsafeFileSystemRepresentation { path in 53 | try attempt { git_clone(&pointer, remoteURLString, path, &options) } 54 | } 55 | 56 | return try Repository.open(at: localURL) 57 | } 58 | 59 | // MARK: - 60 | 61 | public subscript(_ shorthand: String) -> T? { 62 | var pointer: OpaquePointer? 63 | guard case .success = result(of: { git_reference_dwim(&pointer, self.pointer, shorthand) }), 64 | pointer != nil 65 | else { return nil } 66 | 67 | return T(pointer!) 68 | } 69 | 70 | /** 71 | The repository's working directory. 72 | 73 | For example, `path/to/repository/.git`. 74 | */ 75 | public var commonDirectory: URL? { 76 | let path = String(cString: git_repository_commondir(pointer)) 77 | return URL(fileURLWithPath: path, isDirectory: true) 78 | } 79 | 80 | /** 81 | The repository's working directory, 82 | or `nil` if the repository is bare. 83 | 84 | For example, `path/to/repository`. 85 | */ 86 | public var workingDirectory: URL? { 87 | let path = String(cString: git_repository_workdir(pointer)) 88 | return URL(fileURLWithPath: path, isDirectory: true) 89 | } 90 | 91 | /// The repository index, if any. 92 | public lazy var index: Index? = { 93 | var pointer: OpaquePointer? 94 | guard case .success = result(of: { git_repository_index(&pointer, self.pointer) }), 95 | pointer != nil 96 | else { return nil } 97 | let index = Index(pointer!) 98 | index.managed = true 99 | 100 | return index 101 | }() 102 | 103 | /// The `HEAD` of the repository. 104 | public var head: Head? { 105 | var pointer: OpaquePointer? 106 | guard case .success = result(of: { git_repository_head(&pointer, self.pointer) }) else { return nil } 107 | 108 | if git_repository_head_detached(self.pointer) != 0 { 109 | return .detached(Commit(pointer!)) 110 | } else { 111 | return .attached(Branch(pointer!)) 112 | } 113 | } 114 | 115 | /// Returns a branch by name. 116 | public func branch(named name: String) throws -> Branch? { 117 | var pointer: OpaquePointer? 118 | try attempt { git_reference_lookup(&pointer, self.pointer, name) } 119 | 120 | guard git_reference_is_branch(pointer) != 0 || 121 | git_reference_is_remote(pointer) != 0 122 | else { 123 | return nil 124 | } 125 | 126 | return Branch(pointer!) 127 | } 128 | 129 | /** 130 | Lookup an object by ID. 131 | 132 | - Parameters: 133 | - id: The object ID. 134 | - Throws: An error if no object exists for the 135 | - Returns: The corresponding object. 136 | */ 137 | func lookup(type: T.Type, with id: Object.ID) throws -> T? { 138 | var pointer: OpaquePointer? 139 | var oid = id.rawValue 140 | try attempt { git_object_lookup(&pointer, self.pointer, &oid, T.type) } 141 | // defer { git_object_free(pointer) } 142 | 143 | switch type { 144 | case is Blob.Type, 145 | is Commit.Type, 146 | is Tree.Type: 147 | return T(pointer!) 148 | default: 149 | return Object.type(of: pointer)?.init(pointer!) as? T 150 | } 151 | } 152 | 153 | func lookup(type: T.Type, named name: String) throws -> T? { 154 | var pointer: OpaquePointer? 155 | try attempt { git_reference_lookup(&pointer, self.pointer, name) } 156 | return T(pointer!) 157 | } 158 | 159 | /** 160 | Returns the revision matching the provided specification. 161 | 162 | - Parameters: 163 | - specification: A revision specification. 164 | - Returns: A tuple containing the commit and/or reference 165 | matching the specification. 166 | */ 167 | public func revision(matching specification: String) throws -> (Commit?, Reference?) { 168 | var commitPointer: OpaquePointer? 169 | var referencePointer: OpaquePointer? 170 | 171 | try specification.withCString { string in 172 | try attempt { git_revparse_ext(&commitPointer, &referencePointer, pointer, string) } 173 | } 174 | 175 | return (commitPointer.map(Commit.init), referencePointer.map(Reference.init)) 176 | } 177 | 178 | /** 179 | Calculates the number of unique revisions between two commits. 180 | 181 | - Parameters: 182 | - local: The local commit. 183 | - upstream: The upstream commit. 184 | - Returns: A tuple with the number of commits `ahead` and `behind`. 185 | */ 186 | public func distance(from local: Commit, to upstream: Commit) throws -> (ahead: Int, behind: Int) { 187 | var ahead: Int = 0, behind: Int = 0 188 | var localOID = local.id.rawValue, upstreamOID = upstream.id.rawValue 189 | try attempt { git_graph_ahead_behind(&ahead, &behind, pointer, &localOID, &upstreamOID) } 190 | return (ahead, behind) 191 | } 192 | 193 | @discardableResult 194 | public func createCommit(message: String, author: Signature? = nil, committer: Signature? = nil) throws -> Commit { 195 | let tree = try lookup(type: Tree.self, with: try Object.ID { oid in 196 | try attempt { git_index_write_tree(oid, index?.pointer) } 197 | }) 198 | 199 | var committer = (try committer ?? author ?? Signature.default(for: self)).rawValue 200 | var author = (try author ?? Signature.default(for: self)).rawValue 201 | 202 | var parents = [head?.commit].compactMap { $0?.pointer } as [OpaquePointer?] 203 | 204 | return try lookup(type: Commit.self, with: try Object.ID { oid in 205 | try attempt { git_commit_create(oid, pointer, "HEAD", &author, &committer, "UTF-8", message, tree?.pointer, parents.count, &parents) } 206 | })! 207 | } 208 | 209 | /// Creates a lightweight tag. 210 | public func createLightweightTag(named name: String, target: Object, force: Bool = false) throws { 211 | let _ = try Object.ID { oid in 212 | try attempt { git_tag_create_lightweight(oid, self.pointer, name, target.pointer, force ? 1 : 0) } 213 | } 214 | } 215 | 216 | /// Creates an annotated tag. 217 | public func createAnnotatedTag(named name: String, target: Object, tagger: Signature? = nil, message: String, force: Bool = false) throws { 218 | var signature = try (tagger ?? Signature.default(for: self)).rawValue 219 | let _ = try Object.ID { oid in 220 | try attempt { git_tag_create(oid, self.pointer, name, target.pointer, &signature, message, force ? 1 : 0) } 221 | } 222 | } 223 | 224 | public func tagNames() throws -> [String] { 225 | let pointer = UnsafeMutablePointer.allocate(capacity: 1) 226 | defer { pointer.deallocate() } 227 | try attempt { git_tag_list(pointer, self.pointer) } 228 | return Array(pointer.pointee) 229 | } 230 | 231 | public func tagNames(matching pattern: String) throws -> [String] { 232 | let pointer = UnsafeMutablePointer.allocate(capacity: 1) 233 | defer { pointer.deallocate() } 234 | // let pattern = try Reference.normalize(name: pattern, format: .refspecPattern) 235 | try attempt { git_tag_list_match(pointer, pattern, self.pointer) } 236 | return Array(pointer.pointee) 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /Sources/Git/Repository/Checkout.swift: -------------------------------------------------------------------------------- 1 | import Clibgit2 2 | import Foundation 3 | 4 | extension Repository { 5 | public enum Checkout { 6 | public enum Strategy { 7 | case force 8 | case safe 9 | } 10 | 11 | public enum ConflictResolution{ 12 | case skipUnmerged 13 | case useOurs 14 | case useTheirs 15 | } 16 | 17 | public struct Configuration { 18 | var rawValue: git_checkout_options 19 | 20 | public static var `default` = try! Configuration() 21 | 22 | init() throws { 23 | let pointer = UnsafeMutablePointer.allocate(capacity: 1) 24 | defer { pointer.deallocate() } 25 | try attempt { git_checkout_options_init(pointer, numericCast(GIT_CHECKOUT_OPTIONS_VERSION)) } 26 | rawValue = pointer.pointee 27 | } 28 | 29 | init(rawValue: git_checkout_options) { 30 | self.rawValue = rawValue 31 | } 32 | 33 | /// Don't apply filters like CRLF conversion 34 | public var disableFilters: Bool { 35 | get { 36 | return rawValue.disable_filters != 0 37 | } 38 | 39 | set { 40 | rawValue.disable_filters = newValue ? 1 : 0 41 | } 42 | } 43 | 44 | /// Default is 0755 45 | public var directoryMode: Int { 46 | get { 47 | return numericCast(rawValue.dir_mode) 48 | } 49 | 50 | set { 51 | rawValue.dir_mode = numericCast(newValue) 52 | } 53 | } 54 | 55 | /// Default is 0644 or 0755 as dictated by blob 56 | public var fileMode: Int { 57 | get { 58 | return numericCast(rawValue.file_mode) 59 | } 60 | 61 | set { 62 | rawValue.file_mode = numericCast(newValue) 63 | } 64 | } 65 | 66 | // MARK: Strategy 67 | 68 | /// Default will be a safe checkout 69 | public var strategy: Strategy? { 70 | get { 71 | return Strategy?(rawValue: rawValue.checkout_strategy) 72 | } 73 | 74 | set { 75 | rawValue.checkout_strategy = newValue.rawValue | conflictResolution.rawValue | 76 | (allowConflicts ? GIT_CHECKOUT_ALLOW_CONFLICTS.rawValue : 0) | 77 | (removeUntracked ? GIT_CHECKOUT_REMOVE_UNTRACKED.rawValue : 0) | 78 | (removeIgnored ? GIT_CHECKOUT_REMOVE_IGNORED.rawValue : 0) | 79 | (updateOnly ? GIT_CHECKOUT_UPDATE_ONLY.rawValue : 0) | 80 | (updateIndex ? 0 : GIT_CHECKOUT_DONT_UPDATE_INDEX.rawValue) | 81 | (refreshIndex ? 0 : GIT_CHECKOUT_NO_REFRESH.rawValue) | 82 | (overwriteIgnored ? 0 : GIT_CHECKOUT_DONT_OVERWRITE_IGNORED.rawValue) | 83 | (removeExisting ? 0 : GIT_CHECKOUT_DONT_REMOVE_EXISTING.rawValue) 84 | } 85 | } 86 | 87 | /// makes SAFE mode apply safe file updates even if there are conflicts (instead of cancelling the checkout). 88 | public var allowConflicts: Bool { 89 | get { 90 | rawValue.checkout_strategy & GIT_CHECKOUT_ALLOW_CONFLICTS.rawValue != 0 91 | } 92 | 93 | set { 94 | rawValue.checkout_strategy |= GIT_CHECKOUT_ALLOW_CONFLICTS.rawValue 95 | } 96 | } 97 | 98 | public var conflictResolution: ConflictResolution? { 99 | get { 100 | return ConflictResolution?(rawValue: rawValue.checkout_strategy) 101 | } 102 | 103 | set { 104 | rawValue.checkout_strategy = strategy.rawValue | newValue.rawValue | 105 | (allowConflicts ? GIT_CHECKOUT_ALLOW_CONFLICTS.rawValue : 0) | 106 | (removeUntracked ? GIT_CHECKOUT_REMOVE_UNTRACKED.rawValue : 0) | 107 | (removeIgnored ? GIT_CHECKOUT_REMOVE_IGNORED.rawValue : 0) | 108 | (updateOnly ? GIT_CHECKOUT_UPDATE_ONLY.rawValue : 0) | 109 | (updateIndex ? 0 : GIT_CHECKOUT_DONT_UPDATE_INDEX.rawValue) | 110 | (refreshIndex ? 0 : GIT_CHECKOUT_NO_REFRESH.rawValue) | 111 | (overwriteIgnored ? 0 : GIT_CHECKOUT_DONT_OVERWRITE_IGNORED.rawValue) | 112 | (removeExisting ? 0 : GIT_CHECKOUT_DONT_REMOVE_EXISTING.rawValue) 113 | } 114 | } 115 | 116 | /// means remove untracked files (i.e. not in target, baseline, or index, and not ignored) from the working dir. 117 | public var removeUntracked: Bool { 118 | get { 119 | rawValue.checkout_strategy & GIT_CHECKOUT_REMOVE_UNTRACKED.rawValue != 0 120 | } 121 | 122 | set { 123 | if newValue { 124 | rawValue.checkout_strategy |= GIT_CHECKOUT_REMOVE_UNTRACKED.rawValue 125 | } else { 126 | rawValue.checkout_strategy &= ~GIT_CHECKOUT_REMOVE_UNTRACKED.rawValue 127 | } 128 | } 129 | } 130 | 131 | /// means remove ignored files (that are also untracked) from the working directory as well. 132 | public var removeIgnored: Bool { 133 | get { 134 | rawValue.checkout_strategy & GIT_CHECKOUT_REMOVE_IGNORED.rawValue != 0 135 | } 136 | 137 | set { 138 | if newValue { 139 | rawValue.checkout_strategy |= GIT_CHECKOUT_REMOVE_IGNORED.rawValue 140 | } else { 141 | rawValue.checkout_strategy &= ~GIT_CHECKOUT_REMOVE_IGNORED.rawValue 142 | } 143 | } 144 | } 145 | 146 | /// means to only update the content of files that already exist. Files will not be created nor deleted. This just skips applying adds, deletes, and typechanges. 147 | public var updateOnly: Bool { 148 | get { 149 | rawValue.checkout_strategy & GIT_CHECKOUT_UPDATE_ONLY.rawValue != 0 150 | } 151 | 152 | set { 153 | if newValue { 154 | rawValue.checkout_strategy |= GIT_CHECKOUT_UPDATE_ONLY.rawValue 155 | } else { 156 | rawValue.checkout_strategy &= ~GIT_CHECKOUT_UPDATE_ONLY.rawValue 157 | } 158 | } 159 | } 160 | 161 | /// !prevents checkout from writing the updated files' information to the index. 162 | public var updateIndex: Bool { 163 | get { 164 | rawValue.checkout_strategy & GIT_CHECKOUT_DONT_UPDATE_INDEX.rawValue == 0 165 | } 166 | 167 | set { 168 | if newValue { 169 | rawValue.checkout_strategy &= ~GIT_CHECKOUT_DONT_UPDATE_INDEX.rawValue 170 | } else { 171 | rawValue.checkout_strategy |= GIT_CHECKOUT_DONT_UPDATE_INDEX.rawValue 172 | } 173 | } 174 | } 175 | 176 | /// checkout will reload the index and git attributes from disk before any operations. 177 | /// Set to false to disable. 178 | public var refreshIndex: Bool { 179 | get { 180 | rawValue.checkout_strategy & GIT_CHECKOUT_NO_REFRESH.rawValue == 0 181 | } 182 | 183 | set { 184 | if newValue { 185 | rawValue.checkout_strategy &= ~GIT_CHECKOUT_NO_REFRESH.rawValue 186 | } else { 187 | rawValue.checkout_strategy |= GIT_CHECKOUT_NO_REFRESH.rawValue 188 | } 189 | } 190 | } 191 | 192 | /// !prevents ignored files from being overwritten. Normally, files that are ignored in the working directory are not considered "precious" and may be overwritten if the checkout target contains that file. 193 | public var overwriteIgnored: Bool { 194 | get { 195 | rawValue.checkout_strategy & GIT_CHECKOUT_DONT_OVERWRITE_IGNORED.rawValue == 0 196 | } 197 | 198 | set { 199 | if newValue { 200 | rawValue.checkout_strategy &= ~GIT_CHECKOUT_DONT_OVERWRITE_IGNORED.rawValue 201 | } else { 202 | rawValue.checkout_strategy |= GIT_CHECKOUT_DONT_OVERWRITE_IGNORED.rawValue 203 | } 204 | } 205 | } 206 | 207 | /// !prevents checkout from removing files or folders that fold to the same name on case insensitive filesystems. This can cause files to retain their existing names and write through existing symbolic links. 208 | public var removeExisting: Bool { 209 | get { 210 | rawValue.checkout_strategy & GIT_CHECKOUT_DONT_REMOVE_EXISTING.rawValue == 0 211 | } 212 | 213 | set { 214 | if newValue { 215 | rawValue.checkout_strategy &= ~GIT_CHECKOUT_ALLOW_CONFLICTS.rawValue 216 | } else { 217 | rawValue.checkout_strategy |= GIT_CHECKOUT_ALLOW_CONFLICTS.rawValue 218 | } 219 | } 220 | } 221 | } 222 | } 223 | } 224 | 225 | extension Optional /*: internal RawRepresentable */ where Wrapped == Repository.Checkout.Strategy { 226 | init(rawValue: UInt32) { 227 | if rawValue & GIT_CHECKOUT_FORCE.rawValue != 0 { 228 | self = .force 229 | } else if rawValue & GIT_CHECKOUT_SAFE.rawValue != 0 { 230 | self = .safe 231 | } else { 232 | self = .none 233 | } 234 | } 235 | 236 | var rawValue: UInt32 { 237 | switch self { 238 | case .force?: 239 | return GIT_CHECKOUT_FORCE.rawValue 240 | case .safe?: 241 | return GIT_CHECKOUT_SAFE.rawValue 242 | default: 243 | return GIT_CHECKOUT_NONE.rawValue 244 | } 245 | } 246 | } 247 | 248 | extension Optional /*: internal RawRepresentable */ where Wrapped == Repository.Checkout.ConflictResolution { 249 | init(rawValue: UInt32) { 250 | if rawValue & GIT_CHECKOUT_SKIP_UNMERGED.rawValue != 0 { 251 | self = .skipUnmerged 252 | } else if rawValue & GIT_CHECKOUT_USE_OURS.rawValue != 0 { 253 | self = .useOurs 254 | } else if rawValue & GIT_CHECKOUT_USE_THEIRS.rawValue != 0 { 255 | self = .useTheirs 256 | } else { 257 | self = .none 258 | } 259 | } 260 | 261 | var rawValue: UInt32 { 262 | switch self { 263 | case .skipUnmerged?: 264 | return GIT_CHECKOUT_SKIP_UNMERGED.rawValue 265 | case .useOurs?: 266 | return GIT_CHECKOUT_USE_OURS.rawValue 267 | case .useTheirs?: 268 | return GIT_CHECKOUT_USE_THEIRS.rawValue 269 | default: 270 | return GIT_CHECKOUT_NONE.rawValue 271 | } 272 | } 273 | } 274 | --------------------------------------------------------------------------------