├── Sources ├── Clibgit2 │ ├── shim.h │ └── module.modulemap └── Git │ ├── Libgit2.swift │ ├── Remotes.swift │ ├── Info.plist │ ├── OID.swift │ ├── Errors.swift │ ├── Credentials.swift │ ├── CommitIterator.swift │ ├── Pointers.swift │ ├── Diffs.swift │ ├── CheckoutStrategy.swift │ ├── References.swift │ ├── Objects.swift │ └── Repository.swift ├── .hound.yml ├── Tests ├── GitTests │ ├── Fixtures │ │ ├── Mantle.zip │ │ ├── detached-head.zip │ │ ├── simple-repository.zip │ │ ├── repository-with-status.zip │ │ └── Fixtures.swift │ ├── ResultShims.swift │ ├── FixturesSpec.swift │ ├── Info.plist │ ├── RemotesSpec.swift │ ├── OIDSpec.swift │ ├── ReferencesSpec.swift │ ├── ObjectsSpec.swift │ └── RepositorySpec.swift └── LinuxMain.swift ├── .github └── workflows │ └── linux.yml ├── .gitignore ├── Package.resolved ├── LICENSE.md ├── Scripts └── xcode_functions.sh ├── Package.swift ├── .swiftlint.yml └── README.md /Sources/Clibgit2/shim.h: -------------------------------------------------------------------------------- 1 | #include 2 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | # Configuration for Hound (https://houndci.com) 2 | 3 | swiftlint: 4 | config_file: .swiftlint.yml 5 | -------------------------------------------------------------------------------- /Tests/GitTests/Fixtures/Mantle.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/swift-git/HEAD/Tests/GitTests/Fixtures/Mantle.zip -------------------------------------------------------------------------------- /Sources/Clibgit2/module.modulemap: -------------------------------------------------------------------------------- 1 | module Clibgit2 [system] { 2 | header "shim.h" 3 | link "git2" 4 | export * 5 | } 6 | -------------------------------------------------------------------------------- /Tests/GitTests/Fixtures/detached-head.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/swift-git/HEAD/Tests/GitTests/Fixtures/detached-head.zip -------------------------------------------------------------------------------- /Tests/GitTests/Fixtures/simple-repository.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/swift-git/HEAD/Tests/GitTests/Fixtures/simple-repository.zip -------------------------------------------------------------------------------- /Tests/GitTests/Fixtures/repository-with-status.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/swift-git/HEAD/Tests/GitTests/Fixtures/repository-with-status.zip -------------------------------------------------------------------------------- /Tests/GitTests/ResultShims.swift: -------------------------------------------------------------------------------- 1 | // Once Nimble adds matchers for the Result type, remove these shims and refactor the tests that use them. 2 | extension Result { 3 | var value: Success? { 4 | guard case .success(let value) = self else { 5 | return nil 6 | } 7 | return value 8 | } 9 | 10 | var error: Failure? { 11 | guard case .failure(let error) = self else { 12 | return nil 13 | } 14 | return error 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Tests/GitTests/FixturesSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FixturesSpec.swift 3 | // SwiftGit2 4 | // 5 | // Created by Matt Diephouse on 11/16/14. 6 | // Copyright (c) 2014 GitHub, Inc. All rights reserved. 7 | // 8 | 9 | import Quick 10 | 11 | class FixturesSpec: QuickSpec { 12 | override func spec() { 13 | beforeSuite { 14 | Fixtures.sharedInstance.setUp() 15 | } 16 | 17 | afterSuite { 18 | Fixtures.sharedInstance.tearDown() 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Quick 3 | 4 | @testable import GitTests 5 | 6 | QCKMain([ 7 | FixturesSpec.self, 8 | SignatureSpec.self, 9 | CommitSpec.self, 10 | TreeEntrySpec.self, 11 | TreeSpec.self, 12 | BlobSpec.self, 13 | TagSpec.self, 14 | OIDSpec.self, 15 | ReferenceSpec.self, 16 | BranchSpec.self, 17 | TagReferenceSpec.self, 18 | RemoteSpec.self, 19 | RepositorySpec.self, 20 | ]) 21 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: Linux 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-20.04 8 | strategy: 9 | matrix: 10 | swift: ['5.3'] 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: fwal/setup-swift@v1 15 | with: 16 | swift-version: ${{ matrix.swift }} 17 | - name: Install system dependencies 18 | run: sudo apt-get update && sudo apt-get install libgit2-dev 19 | - name: Build 20 | run: swift build 21 | - name: Test 22 | run: swift test 23 | -------------------------------------------------------------------------------- /Sources/Git/Libgit2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Libgit2.swift 3 | // SwiftGit2 4 | // 5 | // Created by Matt Diephouse on 1/11/15. 6 | // Copyright (c) 2015 GitHub, Inc. All rights reserved. 7 | // 8 | 9 | import Clibgit2 10 | 11 | extension git_strarray { 12 | func filter(_ isIncluded: (String) -> Bool) -> [String] { 13 | return map { $0 }.filter(isIncluded) 14 | } 15 | 16 | func map(_ transform: (String) -> T) -> [T] { 17 | return (0.. 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .swiftpm/ 2 | .vscode/ 3 | .build/ 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | *.xcscmblueprint 24 | *.idea* 25 | 26 | External/libgit2*.a 27 | External/ios-openssl 28 | External/libgit2-ios 29 | External/libssh2-ios 30 | 31 | ### fastlane ### 32 | # fastlane - A streamlined workflow tool for Cocoa deployment 33 | 34 | # fastlane specific 35 | fastlane/report.xml 36 | 37 | # deliver temporary files 38 | fastlane/Preview.html 39 | 40 | # snapshot generated screenshots 41 | fastlane/screenshots/**/*.png 42 | fastlane/screenshots/screenshots.html 43 | 44 | # scan temporary files 45 | fastlane/test_output 46 | test_output 47 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Nimble", 6 | "repositoryURL": "https://github.com/Quick/Nimble", 7 | "state": { 8 | "branch": null, 9 | "revision": "bbc079f558ccadfa9cd430c6c8d250d93bbccff1", 10 | "version": "8.0.8" 11 | } 12 | }, 13 | { 14 | "package": "Quick", 15 | "repositoryURL": "https://github.com/Quick/Quick", 16 | "state": { 17 | "branch": null, 18 | "revision": "33682c2f6230c60614861dfc61df267e11a1602f", 19 | "version": "2.2.0" 20 | } 21 | }, 22 | { 23 | "package": "Zip", 24 | "repositoryURL": "https://github.com/marmelroy/Zip.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "80b1c3005ee25b4c7ce46c4029ac3347e8d5e37e", 28 | "version": "2.0.0" 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Git/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2014 GitHub, Inc. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014-2016 SwiftGit2 contributors 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. 22 | -------------------------------------------------------------------------------- /Scripts/xcode_functions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Returns the version # of xcodebuild 4 | # eg. (4.6.3, 5.0, 5.0.1) 5 | function xcode_version () 6 | { 7 | /usr/bin/xcodebuild -version 2> /dev/null | head -n 1 | awk '{ print $2 }' 8 | } 9 | 10 | # Returns the major version of xcodebuild 11 | # eg. (4, 5, 6) 12 | function xcode_major_version () 13 | { 14 | xcode_version | awk -F '.' '{ print $1 }' 15 | } 16 | 17 | # Returns the latest iOS SDK version available via xcodebuild. 18 | function ios_sdk_version () 19 | { 20 | # The grep command produces output like the following, singling out the 21 | # SDKVersion of just the iPhone* SDKs: 22 | # 23 | # iPhoneOS9.0.sdk - iOS 9.0 (iphoneos9.0) 24 | # SDKVersion: 9.0 25 | # -- 26 | # iPhoneSimulator9.0.sdk - Simulator - iOS 9.0 (iphonesimulator9.0) 27 | # SDKVersion: 9.0 28 | 29 | /usr/bin/xcodebuild -version -sdk 2> /dev/null | grep -A 1 '^iPhone' | tail -n 1 | awk '{ print $2 }' 30 | } 31 | 32 | # Returns the path to the specified iOS SDK name 33 | function ios_sdk_path () 34 | { 35 | /usr/bin/xcodebuild -version -sdk 2> /dev/null | grep -i $1 | grep 'Path:' | awk '{ print $2 }' 36 | } 37 | -------------------------------------------------------------------------------- /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: "swift-git", 8 | platforms: [ 9 | .macOS(.v10_11), 10 | .iOS("9.2"), 11 | ], 12 | products: [ 13 | .library( 14 | name: "Git", 15 | targets: ["Git"] 16 | ), 17 | ], 18 | dependencies: [ 19 | .package(url: "https://github.com/Quick/Quick", from: "2.2.0"), 20 | .package(url: "https://github.com/Quick/Nimble", from: "8.0.8"), 21 | .package(url: "https://github.com/marmelroy/Zip.git", from: "2.0.0"), 22 | ], 23 | targets: [ 24 | .systemLibrary( 25 | name: "Clibgit2", 26 | pkgConfig: "libgit2", 27 | providers: [ 28 | .brew(["libgit2"]), 29 | .apt(["libgit2-dev"]), 30 | ] 31 | ), 32 | .target( 33 | name: "Git", 34 | dependencies: [ 35 | .target(name: "Clibgit2"), 36 | ], 37 | exclude: ["Info.plist"] 38 | ), 39 | .testTarget( 40 | name: "GitTests", 41 | dependencies: [ 42 | .target(name: "Git"), 43 | .product(name: "Quick", package: "Quick"), 44 | .product(name: "Nimble", package: "Nimble"), 45 | .product(name: "Zip", package: "Zip"), 46 | ], 47 | exclude: ["Info.plist"], 48 | resources: [ 49 | .copy("Fixtures/repository-with-status.zip"), 50 | .copy("Fixtures/Mantle.zip"), 51 | .copy("Fixtures/simple-repository.zip"), 52 | .copy("Fixtures/detached-head.zip"), 53 | ] 54 | ), 55 | ] 56 | ) 57 | -------------------------------------------------------------------------------- /Sources/Git/OID.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OID.swift 3 | // SwiftGit2 4 | // 5 | // Created by Matt Diephouse on 11/17/14. 6 | // Copyright (c) 2014 GitHub, Inc. All rights reserved. 7 | // 8 | 9 | import Clibgit2 10 | 11 | /// An identifier for a Git object. 12 | public struct OID { 13 | 14 | // MARK: - Initializers 15 | 16 | /// Create an instance from a hex formatted string. 17 | /// 18 | /// string - A 40-byte hex formatted string. 19 | public init?(string: String) { 20 | // libgit2 doesn't enforce a maximum length 21 | if string.lengthOfBytes(using: String.Encoding.ascii) > 40 { 22 | return nil 23 | } 24 | 25 | let pointer = UnsafeMutablePointer.allocate(capacity: 1) 26 | let result = git_oid_fromstr(pointer, string) 27 | 28 | if result < GIT_OK.rawValue { 29 | pointer.deallocate() 30 | return nil 31 | } 32 | 33 | oid = pointer.pointee 34 | pointer.deallocate() 35 | } 36 | 37 | /// Create an instance from a libgit2 `git_oid`. 38 | public init(_ oid: git_oid) { 39 | self.oid = oid 40 | } 41 | 42 | // MARK: - Properties 43 | 44 | public let oid: git_oid 45 | } 46 | 47 | extension OID: CustomStringConvertible { 48 | public var description: String { 49 | let length = Int(GIT_OID_RAWSZ) * 2 50 | let string = UnsafeMutablePointer.allocate(capacity: length) 51 | var oid = self.oid 52 | git_oid_fmt(string, &oid) 53 | 54 | return String(bytesNoCopy: string, length: length, encoding: .ascii, freeWhenDone: true)! 55 | } 56 | } 57 | 58 | extension OID: Hashable { 59 | public func hash(into hasher: inout Hasher) { 60 | withUnsafeBytes(of: oid.id) { 61 | hasher.combine(bytes: $0) 62 | } 63 | } 64 | 65 | public static func == (lhs: OID, rhs: OID) -> Bool { 66 | var left = lhs.oid 67 | var right = rhs.oid 68 | return git_oid_cmp(&left, &right) == 0 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/GitTests/RemotesSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RemotesSpec.swift 3 | // SwiftGit2 4 | // 5 | // Created by Matt Diephouse on 1/2/15. 6 | // Copyright (c) 2015 GitHub, Inc. All rights reserved. 7 | // 8 | 9 | import Git 10 | import Nimble 11 | import Quick 12 | import Clibgit2 13 | 14 | private extension Repository { 15 | func withGitRemote(named name: String, transform: (OpaquePointer) -> T) -> T { 16 | let repository = self.pointer 17 | 18 | var pointer: OpaquePointer? = nil 19 | git_remote_lookup(&pointer, repository, name) 20 | let result = transform(pointer!) 21 | git_remote_free(pointer) 22 | 23 | return result 24 | } 25 | } 26 | 27 | class RemoteSpec: FixturesSpec { 28 | override func spec() { 29 | describe("Remote(pointer)") { 30 | it("should initialize its properties") { 31 | let repo = Fixtures.mantleRepository 32 | let remote = repo.withGitRemote(named: "upstream") { Remote($0) } 33 | 34 | expect(remote.name).to(equal("upstream")) 35 | expect(remote.URL).to(equal("git@github.com:Mantle/Mantle.git")) 36 | } 37 | } 38 | 39 | describe("==(Remote, Remote)") { 40 | it("should be true with equal objects") { 41 | let repo = Fixtures.mantleRepository 42 | let remote1 = repo.withGitRemote(named: "upstream") { Remote($0) } 43 | let remote2 = remote1 44 | expect(remote1).to(equal(remote2)) 45 | } 46 | 47 | it("should be false with unequal objcets") { 48 | let repo = Fixtures.mantleRepository 49 | let origin = repo.withGitRemote(named: "origin") { Remote($0) } 50 | let upstream = repo.withGitRemote(named: "upstream") { Remote($0) } 51 | expect(origin).notTo(equal(upstream)) 52 | } 53 | } 54 | 55 | describe("Remote.hashValue") { 56 | it("should be equal with equal objcets") { 57 | let repo = Fixtures.mantleRepository 58 | let remote1 = repo.withGitRemote(named: "upstream") { Remote($0) } 59 | let remote2 = remote1 60 | expect(remote1.hashValue).to(equal(remote2.hashValue)) 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Git/Errors.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Clibgit2 3 | 4 | public let libGit2ErrorDomain = "org.libgit2.libgit2" 5 | 6 | internal extension NSError { 7 | /// Returns an NSError with an error domain and message for libgit2 errors. 8 | /// 9 | /// :param: errorCode An error code returned by a libgit2 function. 10 | /// :param: libGit2PointOfFailure The name of the libgit2 function that produced the 11 | /// error code. 12 | /// :returns: An NSError with a libgit2 error domain, code, and message. 13 | convenience init(gitError errorCode: Int32, pointOfFailure: String? = nil) { 14 | let code = Int(errorCode) 15 | var userInfo: [String: String] = [:] 16 | 17 | if let message = errorMessage(errorCode) { 18 | userInfo[NSLocalizedDescriptionKey] = message 19 | } else { 20 | userInfo[NSLocalizedDescriptionKey] = "Unknown libgit2 error." 21 | } 22 | 23 | if let pointOfFailure = pointOfFailure { 24 | userInfo[NSLocalizedFailureReasonErrorKey] = "\(pointOfFailure) failed." 25 | } 26 | 27 | self.init(domain: libGit2ErrorDomain, code: code, userInfo: userInfo) 28 | } 29 | } 30 | 31 | /// Returns the libgit2 error message for the given error code. 32 | /// 33 | /// The error message represents the last error message generated by 34 | /// libgit2 in the current thread. 35 | /// 36 | /// :param: errorCode An error code returned by a libgit2 function. 37 | /// :returns: If the error message exists either in libgit2's thread-specific registry, 38 | /// or errno has been set by the system, this function returns the 39 | /// corresponding string representation of that error. Otherwise, it returns 40 | /// nil. 41 | private func errorMessage(_ errorCode: Int32) -> String? { 42 | let last = giterr_last() 43 | if let lastErrorPointer = last { 44 | return String(validatingUTF8: lastErrorPointer.pointee.message) 45 | } else if UInt32(errorCode) == GIT_ERROR_OS.rawValue { 46 | return String(validatingUTF8: strerror(errno)) 47 | } else { 48 | return nil 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Git/Credentials.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Credentials.swift 3 | // SwiftGit2 4 | // 5 | // Created by Tom Booth on 29/02/2016. 6 | // Copyright © 2016 GitHub, Inc. All rights reserved. 7 | // 8 | 9 | import Clibgit2 10 | 11 | private class Wrapper { 12 | let value: T 13 | 14 | init(_ value: T) { 15 | self.value = value 16 | } 17 | } 18 | 19 | public enum Credentials { 20 | case `default` 21 | case sshAgent 22 | case plaintext(username: String, password: String) 23 | case sshMemory(username: String, publicKey: String, privateKey: String, passphrase: String) 24 | 25 | internal static func fromPointer(_ pointer: UnsafeMutableRawPointer) -> Credentials { 26 | return Unmanaged>.fromOpaque(UnsafeRawPointer(pointer)).takeRetainedValue().value 27 | } 28 | 29 | internal func toPointer() -> UnsafeMutableRawPointer { 30 | return Unmanaged.passRetained(Wrapper(self)).toOpaque() 31 | } 32 | } 33 | 34 | /// Handle the request of credentials, passing through to a wrapped block after converting the arguments. 35 | /// Converts the result to the correct error code required by libgit2 (0 = success, 1 = rejected setting creds, 36 | /// -1 = error) 37 | internal func credentialsCallback( 38 | cred: UnsafeMutablePointer?>?, 39 | url: UnsafePointer?, 40 | username: UnsafePointer?, 41 | _: UInt32, 42 | payload: UnsafeMutableRawPointer? ) -> Int32 { 43 | 44 | let result: Int32 45 | 46 | // Find username_from_url 47 | let name = username.map(String.init(cString:)) 48 | 49 | switch Credentials.fromPointer(payload!) { 50 | case .default: 51 | result = git_cred_default_new(cred) 52 | case .sshAgent: 53 | result = git_cred_ssh_key_from_agent(cred, name!) 54 | case .plaintext(let username, let password): 55 | result = git_cred_userpass_plaintext_new(cred, username, password) 56 | case .sshMemory(let username, let publicKey, let privateKey, let passphrase): 57 | result = git_cred_ssh_key_memory_new(cred, username, publicKey, privateKey, passphrase) 58 | } 59 | 60 | return (result != GIT_OK.rawValue) ? -1 : 0 61 | } 62 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - file_length 3 | - force_cast 4 | - force_try 5 | - function_body_length 6 | - identifier_name 7 | - redundant_optional_initialization 8 | - type_body_length 9 | 10 | opt_in_rules: 11 | - array_init 12 | - attributes 13 | - closure_end_indentation 14 | - closure_spacing 15 | - collection_alignment 16 | - conditional_returns_on_newline 17 | - contains_over_first_not_nil 18 | - convenience_type 19 | - discouraged_object_literal 20 | - discouraged_optional_boolean 21 | - discouraged_optional_collection 22 | - empty_count 23 | - empty_string 24 | - empty_xctest_method 25 | - explicit_enum_raw_value 26 | - explicit_init 27 | - extension_access_modifier 28 | - fallthrough 29 | - fatal_error_message 30 | - first_where 31 | - function_default_parameter_at_end 32 | - identical_operands 33 | - implicitly_unwrapped_optional 34 | - joined_default_parameter 35 | - last_where 36 | - legacy_random 37 | - literal_expression_end_indentation 38 | - lower_acl_than_parent 39 | - modifier_order 40 | - multiline_arguments 41 | - multiline_function_chains 42 | - multiline_literal_brackets 43 | - nslocalizedstring_key 44 | - overridden_super_call 45 | - override_in_extension 46 | - private_action 47 | - private_outlet 48 | - prohibited_super_call 49 | - quick_discouraged_call 50 | - quick_discouraged_focused_test 51 | - quick_discouraged_pending_test 52 | - redundant_nil_coalescing 53 | - redundant_type_annotation 54 | - sorted_first_last 55 | - static_operator 56 | - strict_fileprivate 57 | - switch_case_on_newline 58 | - toggle_bool 59 | - unavailable_function 60 | - untyped_error_in_catch 61 | - unused_import 62 | - unused_private_declaration 63 | - vertical_parameter_alignment_on_call 64 | - xct_specific_matcher 65 | - yoda_condition 66 | 67 | excluded: 68 | - Carthage 69 | - External 70 | 71 | line_length: 72 | ignores_function_declarations: true 73 | 74 | trailing_comma: 75 | mandatory_comma: true 76 | 77 | trailing_whitespace: 78 | ignores_comments: false 79 | ignores_empty_lines: false 80 | -------------------------------------------------------------------------------- /Sources/Git/CommitIterator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Arnon Keereena on 4/28/17. 3 | // Copyright (c) 2017 GitHub, Inc. All rights reserved. 4 | // 5 | 6 | import Foundation 7 | import Clibgit2 8 | 9 | public class CommitIterator: IteratorProtocol, Sequence { 10 | public typealias Iterator = CommitIterator 11 | public typealias Element = Result 12 | let repo: Repository 13 | private var revisionWalker: OpaquePointer? 14 | 15 | private enum Next { 16 | case over 17 | case okay 18 | case error(NSError) 19 | 20 | init(_ result: Int32, name: String) { 21 | switch result { 22 | case GIT_ITEROVER.rawValue: 23 | self = .over 24 | case GIT_OK.rawValue: 25 | self = .okay 26 | default: 27 | self = .error(NSError(gitError: result, pointOfFailure: name)) 28 | } 29 | } 30 | } 31 | 32 | init(repo: Repository, root: git_oid) { 33 | self.repo = repo 34 | setupRevisionWalker(root: root) 35 | } 36 | 37 | deinit { 38 | git_revwalk_free(self.revisionWalker) 39 | } 40 | 41 | private func setupRevisionWalker(root: git_oid) { 42 | var oid = root 43 | git_revwalk_new(&revisionWalker, repo.pointer) 44 | git_revwalk_sorting(revisionWalker, GIT_SORT_TOPOLOGICAL.rawValue) 45 | git_revwalk_sorting(revisionWalker, GIT_SORT_TIME.rawValue) 46 | git_revwalk_push(revisionWalker, &oid) 47 | } 48 | 49 | public func next() -> Element? { 50 | var oid = git_oid() 51 | let revwalkGitResult = git_revwalk_next(&oid, revisionWalker) 52 | let nextResult = Next(revwalkGitResult, name: "git_revwalk_next") 53 | switch nextResult { 54 | case let .error(error): 55 | return Result.failure(error) 56 | case .over: 57 | return nil 58 | case .okay: 59 | var unsafeCommit: OpaquePointer? = nil 60 | let lookupGitResult = git_commit_lookup(&unsafeCommit, repo.pointer, &oid) 61 | guard lookupGitResult == GIT_OK.rawValue, 62 | let unwrapCommit = unsafeCommit else { 63 | return Result.failure(NSError(gitError: lookupGitResult, pointOfFailure: "git_commit_lookup")) 64 | } 65 | let result: Element = Result.success(Commit(unwrapCommit)) 66 | git_commit_free(unsafeCommit) 67 | return result 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/GitTests/Fixtures/Fixtures.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Fixtures.swift 3 | // SwiftGit2 4 | // 5 | // Created by Matt Diephouse on 11/16/14. 6 | // Copyright (c) 2014 GitHub, Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Git 11 | import Zip 12 | 13 | final class Fixtures { 14 | 15 | // MARK: Lifecycle 16 | 17 | class var sharedInstance: Fixtures { 18 | enum Singleton { 19 | static let instance = Fixtures() 20 | } 21 | return Singleton.instance 22 | } 23 | 24 | init() { 25 | directoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) 26 | .appendingPathComponent("org.libgit2.SwiftGit2") 27 | .appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString) 28 | } 29 | 30 | // MARK: - Setup and Teardown 31 | 32 | let directoryURL: URL 33 | 34 | func setUp() { 35 | try! FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) 36 | 37 | let zipURLs = Bundle.module.urls(forResourcesWithExtension: "zip", subdirectory: nil)! 38 | assert(!zipURLs.isEmpty, "No zip-files for testing found.") 39 | for url in zipURLs { 40 | // Will throw error but everything will be fine. Does not happen when using SwiftPM. 41 | try? Zip.unzipFile(url as URL, destination: directoryURL, overwrite: true, password: nil) 42 | } 43 | } 44 | 45 | func tearDown() { 46 | try? FileManager.default.removeItem(at: directoryURL) 47 | } 48 | 49 | // MARK: - Helpers 50 | 51 | func repository(named name: String) -> Repository { 52 | let url = directoryURL.appendingPathComponent(name, isDirectory: true) 53 | switch Repository.at(url) { 54 | case .success(let repo): 55 | return repo 56 | case .failure(let error): 57 | fatalError(error.localizedDescription) 58 | } 59 | } 60 | 61 | // MARK: - The Fixtures 62 | 63 | class var detachedHeadRepository: Repository { 64 | return Fixtures.sharedInstance.repository(named: "detached-head") 65 | } 66 | 67 | class var simpleRepository: Repository { 68 | return Fixtures.sharedInstance.repository(named: "simple-repository") 69 | } 70 | 71 | class var mantleRepository: Repository { 72 | return Fixtures.sharedInstance.repository(named: "Mantle") 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Tests/GitTests/OIDSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OIDSpec.swift 3 | // SwiftGit2 4 | // 5 | // Created by Matt Diephouse on 11/17/14. 6 | // Copyright (c) 2014 GitHub, Inc. All rights reserved. 7 | // 8 | 9 | import Git 10 | import Nimble 11 | import Quick 12 | 13 | class OIDSpec: QuickSpec { 14 | override func spec() { 15 | describe("OID(string:)") { 16 | it("should be nil if string is too short") { 17 | expect(OID(string: "123456789012345678901234567890123456789")).to(beNil()) 18 | } 19 | 20 | it("should be nil if string is too long") { 21 | expect(OID(string: "12345678901234567890123456789012345678901")).to(beNil()) 22 | } 23 | 24 | it("should not be nil if string is just right") { 25 | expect(OID(string: "1234567890123456789012345678ABCDEFabcdef")).notTo(beNil()) 26 | } 27 | 28 | it("should be nil with non-hex characters") { 29 | expect(OID(string: "123456789012345678901234567890123456789j")).to(beNil()) 30 | } 31 | } 32 | 33 | describe("OID(oid)") { 34 | it("should equal an OID with the same git_oid") { 35 | let oid = OID(string: "1234567890123456789012345678901234567890")! 36 | expect(OID(oid.oid)).to(equal(oid)) 37 | } 38 | } 39 | 40 | describe("OID.description") { 41 | it("should return the SHA") { 42 | let SHA = "1234567890123456789012345678901234567890" 43 | let oid = OID(string: SHA)! 44 | expect(oid.description).to(equal(SHA)) 45 | } 46 | } 47 | 48 | describe("==(OID, OID)") { 49 | it("should be equal when identical") { 50 | let SHA = "1234567890123456789012345678901234567890" 51 | let oid1 = OID(string: SHA)! 52 | let oid2 = OID(string: SHA)! 53 | expect(oid1).to(equal(oid2)) 54 | } 55 | 56 | it("should be not equal when different") { 57 | let oid1 = OID(string: "1234567890123456789012345678901234567890")! 58 | let oid2 = OID(string: "0000000000000000000000000000000000000000")! 59 | expect(oid1).notTo(equal(oid2)) 60 | } 61 | } 62 | 63 | describe("OID.hashValue") { 64 | it("should be equal when OIDs are equal") { 65 | let SHA = "1234567890123456789012345678901234567890" 66 | let oid1 = OID(string: SHA)! 67 | let oid2 = OID(string: SHA)! 68 | expect(oid1.hashValue).to(equal(oid2.hashValue)) 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/Git/Pointers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pointers.swift 3 | // SwiftGit2 4 | // 5 | // Created by Matt Diephouse on 12/23/14. 6 | // Copyright (c) 2014 GitHub, Inc. All rights reserved. 7 | // 8 | 9 | import Clibgit2 10 | 11 | /// A pointer to a git object. 12 | public protocol PointerType: Hashable { 13 | /// The OID of the referenced object. 14 | var oid: OID { get } 15 | 16 | /// The libgit2 `git_object_t` of the referenced object. 17 | var type: git_object_t { get } 18 | } 19 | 20 | public extension PointerType { 21 | static func == (lhs: Self, rhs: Self) -> Bool { 22 | return lhs.oid == rhs.oid 23 | && lhs.type == rhs.type 24 | } 25 | 26 | func hash(into hasher: inout Hasher) { 27 | hasher.combine(oid) 28 | } 29 | } 30 | 31 | /// A pointer to a git object. 32 | public enum Pointer: PointerType { 33 | case commit(OID) 34 | case tree(OID) 35 | case blob(OID) 36 | case tag(OID) 37 | 38 | public var oid: OID { 39 | switch self { 40 | case let .commit(oid): 41 | return oid 42 | case let .tree(oid): 43 | return oid 44 | case let .blob(oid): 45 | return oid 46 | case let .tag(oid): 47 | return oid 48 | } 49 | } 50 | 51 | public var type: git_object_t { 52 | switch self { 53 | case .commit: 54 | return GIT_OBJECT_COMMIT 55 | case .tree: 56 | return GIT_OBJECT_TREE 57 | case .blob: 58 | return GIT_OBJECT_BLOB 59 | case .tag: 60 | return GIT_OBJECT_TAG 61 | } 62 | } 63 | 64 | /// Create an instance with an OID and a libgit2 `git_object_t`. 65 | init?(oid: OID, type: git_object_t) { 66 | switch type { 67 | case GIT_OBJECT_COMMIT: 68 | self = .commit(oid) 69 | case GIT_OBJECT_TREE: 70 | self = .tree(oid) 71 | case GIT_OBJECT_BLOB: 72 | self = .blob(oid) 73 | case GIT_OBJECT_TAG: 74 | self = .tag(oid) 75 | default: 76 | return nil 77 | } 78 | } 79 | } 80 | 81 | extension Pointer: CustomStringConvertible { 82 | public var description: String { 83 | switch self { 84 | case .commit: 85 | return "commit(\(oid))" 86 | case .tree: 87 | return "tree(\(oid))" 88 | case .blob: 89 | return "blob(\(oid))" 90 | case .tag: 91 | return "tag(\(oid))" 92 | } 93 | } 94 | } 95 | 96 | public struct PointerTo: PointerType { 97 | public let oid: OID 98 | 99 | public var type: git_object_t { 100 | return T.type 101 | } 102 | 103 | public init(_ oid: OID) { 104 | self.oid = oid 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift Git 2 | 3 | [![Linux](https://github.com/fwcd/swift-git/actions/workflows/linux.yml/badge.svg)](https://github.com/fwcd/swift-git/actions/workflows/linux.yml) 4 | ![SwiftPM compatible](https://img.shields.io/badge/SwiftPM-compatible-4BC51D.svg?style=flat) 5 | ![Swift 5.3.x](https://img.shields.io/badge/Swift-5.3.x-orange.svg) 6 | 7 | Swift bindings to [libgit2](https://github.com/libgit2/libgit2). 8 | 9 | ```swift 10 | let URL: URL = ... 11 | let result = Repository.at(URL) 12 | switch result { 13 | case let .success(repo): 14 | let latestCommit = repo 15 | .HEAD() 16 | .flatMap { 17 | repo.commit($0.oid) 18 | } 19 | 20 | switch latestCommit { 21 | case let .success(commit): 22 | print("Latest Commit: \(commit.message) by \(commit.author.name)") 23 | 24 | case let .failure(error): 25 | print("Could not get commit: \(error)") 26 | } 27 | 28 | case let .failure(error): 29 | print("Could not open repository: \(error)") 30 | } 31 | ``` 32 | 33 | ## Design 34 | 35 | `swift-git` uses value types wherever possible. That means using Swift’s `struct`s and `enum`s without holding references to libgit2 objects. This has a number of advantages: 36 | 37 | 1. Values can be used concurrently. 38 | 2. Consuming values won’t result in disk access. 39 | 3. Disk access can be contained to a smaller number of APIs. 40 | 41 | This vastly simplifies the design of long-lived applications, which are the most common use case with Swift. Consequently, `swift-git` APIs don’t necessarily map 1-to-1 with libgit2 APIs. 42 | 43 | All methods for reading from or writing to a repository are on `swift-git`’s only `class`: `Repository`. This highlights the failability and mutation of these methods, while freeing up all other instances to be immutable `struct`s and `enum`s. 44 | 45 | ## Required Tools 46 | 47 | To build `swift-git`, you need to install `libgit2` on your system. 48 | 49 | ### on macOS 50 | 51 | Make sure to have Homebrew installed, then run 52 | 53 | ``` 54 | brew install libgit2 55 | ``` 56 | 57 | ### on Linux 58 | 59 | On Debian/Ubuntu-based distributions, run 60 | 61 | ``` 62 | apt install libgit2-dev 63 | ``` 64 | 65 | ## Adding `swift-git` to your Project 66 | 67 | The easiest way to add `swift-git` to your project is to use [SwiftPM](https://swift.org/package-manager/). Simply add the following line to your `Package.swift`'s dependencies: 68 | 69 | ```swift 70 | .package(url: "https://github.com/fwcd/swift-git.git", .branch("main")) 71 | ``` 72 | 73 | ## Building `swift-git` Manually 74 | 75 | If you want to build a copy of `swift-git`, e.g. for development: 76 | 77 | 1. Clone `swift-git` 78 | 2. Run `swift build` 79 | 3. Optionally run `swift test` to run the tests 80 | 81 | ## Contributions 82 | 83 | We :heart: to receive pull requests! GitHub makes it easy: 84 | 85 | 1. Fork the repository 86 | 2. Create a branch with your changes 87 | 3. Send a Pull Request 88 | 89 | All contributions should match GitHub’s [Swift Style Guide](https://github.com/github/swift-style-guide). 90 | 91 | ## License 92 | 93 | `swift-git` is available under the MIT license. 94 | -------------------------------------------------------------------------------- /Sources/Git/Diffs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Diffs.swift 3 | // SwiftGit2 4 | // 5 | // Created by Jake Van Alstyne on 8/20/17. 6 | // Copyright © 2017 GitHub, Inc. All rights reserved. 7 | // 8 | 9 | import Clibgit2 10 | 11 | public struct StatusEntry { 12 | public var status: Diff.Status 13 | public var headToIndex: Diff.Delta? 14 | public var indexToWorkDir: Diff.Delta? 15 | 16 | public init(from statusEntry: git_status_entry) { 17 | self.status = Diff.Status(rawValue: statusEntry.status.rawValue) 18 | 19 | if let htoi = statusEntry.head_to_index { 20 | self.headToIndex = Diff.Delta(htoi.pointee) 21 | } 22 | 23 | if let itow = statusEntry.index_to_workdir { 24 | self.indexToWorkDir = Diff.Delta(itow.pointee) 25 | } 26 | } 27 | } 28 | 29 | public struct Diff { 30 | 31 | /// The set of deltas. 32 | public var deltas = [Delta]() 33 | 34 | public struct Delta { 35 | public static let type = GIT_OBJECT_REF_DELTA 36 | 37 | public var status: Status 38 | public var flags: Flags 39 | public var oldFile: File? 40 | public var newFile: File? 41 | 42 | public init(_ delta: git_diff_delta) { 43 | self.status = Status(rawValue: UInt32(git_diff_status_char(delta.status))) 44 | self.flags = Flags(rawValue: delta.flags) 45 | self.oldFile = File(delta.old_file) 46 | self.newFile = File(delta.new_file) 47 | } 48 | } 49 | 50 | public struct File { 51 | public var oid: OID 52 | public var path: String 53 | public var size: UInt64 54 | public var flags: Flags 55 | 56 | public init(_ diffFile: git_diff_file) { 57 | self.oid = OID(diffFile.id) 58 | let path = diffFile.path 59 | self.path = path.map(String.init(cString:))! 60 | self.size = UInt64(diffFile.size) 61 | self.flags = Flags(rawValue: diffFile.flags) 62 | } 63 | } 64 | 65 | public struct Status: OptionSet { 66 | // This appears to be necessary due to bug in Swift 67 | // https://bugs.swift.org/browse/SR-3003 68 | public init(rawValue: UInt32) { 69 | self.rawValue = rawValue 70 | } 71 | public let rawValue: UInt32 72 | 73 | public static let current = Status(rawValue: GIT_STATUS_CURRENT.rawValue) 74 | public static let indexNew = Status(rawValue: GIT_STATUS_INDEX_NEW.rawValue) 75 | public static let indexModified = Status(rawValue: GIT_STATUS_INDEX_MODIFIED.rawValue) 76 | public static let indexDeleted = Status(rawValue: GIT_STATUS_INDEX_DELETED.rawValue) 77 | public static let indexRenamed = Status(rawValue: GIT_STATUS_INDEX_RENAMED.rawValue) 78 | public static let indexTypeChange = Status(rawValue: GIT_STATUS_INDEX_TYPECHANGE.rawValue) 79 | public static let workTreeNew = Status(rawValue: GIT_STATUS_WT_NEW.rawValue) 80 | public static let workTreeModified = Status(rawValue: GIT_STATUS_WT_MODIFIED.rawValue) 81 | public static let workTreeDeleted = Status(rawValue: GIT_STATUS_WT_DELETED.rawValue) 82 | public static let workTreeTypeChange = Status(rawValue: GIT_STATUS_WT_TYPECHANGE.rawValue) 83 | public static let workTreeRenamed = Status(rawValue: GIT_STATUS_WT_RENAMED.rawValue) 84 | public static let workTreeUnreadable = Status(rawValue: GIT_STATUS_WT_UNREADABLE.rawValue) 85 | public static let ignored = Status(rawValue: GIT_STATUS_IGNORED.rawValue) 86 | public static let conflicted = Status(rawValue: GIT_STATUS_CONFLICTED.rawValue) 87 | } 88 | 89 | public struct Flags: OptionSet { 90 | // This appears to be necessary due to bug in Swift 91 | // https://bugs.swift.org/browse/SR-3003 92 | public init(rawValue: UInt32) { 93 | self.rawValue = rawValue 94 | } 95 | public let rawValue: UInt32 96 | 97 | public static let binary = Flags([]) 98 | public static let notBinary = Flags(rawValue: 1 << 0) 99 | public static let validId = Flags(rawValue: 1 << 1) 100 | public static let exists = Flags(rawValue: 1 << 2) 101 | } 102 | 103 | /// Create an instance with a libgit2 `git_diff`. 104 | public init(_ pointer: OpaquePointer) { 105 | for i in 0.. Bool { 25 | return lhs.longName == rhs.longName 26 | && lhs.oid == rhs.oid 27 | } 28 | 29 | func hash(into hasher: inout Hasher) { 30 | hasher.combine(longName) 31 | hasher.combine(oid) 32 | } 33 | } 34 | 35 | /// Create a Reference, Branch, or TagReference from a libgit2 `git_reference`. 36 | internal func referenceWithLibGit2Reference(_ pointer: OpaquePointer) -> ReferenceType { 37 | if git_reference_is_branch(pointer) != 0 || git_reference_is_remote(pointer) != 0 { 38 | return Branch(pointer)! 39 | } else if git_reference_is_tag(pointer) != 0 { 40 | return TagReference(pointer)! 41 | } else { 42 | return Reference(pointer) 43 | } 44 | } 45 | 46 | /// A generic reference to a git object. 47 | public struct Reference: ReferenceType, Hashable { 48 | /// The full name of the reference (e.g., `refs/heads/master`). 49 | public let longName: String 50 | 51 | /// The short human-readable name of the reference if one exists (e.g., `master`). 52 | public let shortName: String? 53 | 54 | /// The OID of the referenced object. 55 | public let oid: OID 56 | 57 | /// Create an instance with a libgit2 `git_reference` object. 58 | public init(_ pointer: OpaquePointer) { 59 | let shorthand = String(validatingUTF8: git_reference_shorthand(pointer))! 60 | longName = String(validatingUTF8: git_reference_name(pointer))! 61 | shortName = (shorthand == longName ? nil : shorthand) 62 | oid = OID(git_reference_target(pointer).pointee) 63 | } 64 | } 65 | 66 | /// A git branch. 67 | public struct Branch: ReferenceType, Hashable { 68 | /// The full name of the reference (e.g., `refs/heads/master`). 69 | public let longName: String 70 | 71 | /// The short human-readable name of the branch (e.g., `master`). 72 | public let name: String 73 | 74 | /// A pointer to the referenced commit. 75 | public let commit: PointerTo 76 | 77 | // MARK: Derived Properties 78 | 79 | /// The short human-readable name of the branch (e.g., `master`). 80 | /// 81 | /// This is the same as `name`, but is declared with an Optional type to adhere to 82 | /// `ReferenceType`. 83 | public var shortName: String? { return name } 84 | 85 | /// The OID of the referenced object. 86 | /// 87 | /// This is the same as `commit.oid`, but is declared here to adhere to `ReferenceType`. 88 | public var oid: OID { return commit.oid } 89 | 90 | /// Whether the branch is a local branch. 91 | public var isLocal: Bool { return longName.hasPrefix("refs/heads/") } 92 | 93 | /// Whether the branch is a remote branch. 94 | public var isRemote: Bool { return longName.hasPrefix("refs/remotes/") } 95 | 96 | /// Create an instance with a libgit2 `git_reference` object. 97 | /// 98 | /// Returns `nil` if the pointer isn't a branch. 99 | public init?(_ pointer: OpaquePointer) { 100 | var namePointer: UnsafePointer? = nil 101 | let success = git_branch_name(&namePointer, pointer) 102 | guard success == GIT_OK.rawValue else { 103 | return nil 104 | } 105 | name = String(validatingUTF8: namePointer!)! 106 | 107 | longName = String(validatingUTF8: git_reference_name(pointer))! 108 | 109 | var oid: OID 110 | if git_reference_type(pointer).rawValue == GIT_REFERENCE_SYMBOLIC.rawValue { 111 | var resolved: OpaquePointer? = nil 112 | let success = git_reference_resolve(&resolved, pointer) 113 | guard success == GIT_OK.rawValue else { 114 | return nil 115 | } 116 | oid = OID(git_reference_target(resolved).pointee) 117 | git_reference_free(resolved) 118 | } else { 119 | oid = OID(git_reference_target(pointer).pointee) 120 | } 121 | commit = PointerTo(oid) 122 | } 123 | } 124 | 125 | /// A git tag reference, which can be either a lightweight tag or a Tag object. 126 | public enum TagReference: ReferenceType, Hashable { 127 | /// A lightweight tag, which is just a name and an OID. 128 | case lightweight(String, OID) 129 | 130 | /// An annotated tag, which points to a Tag object. 131 | case annotated(String, Tag) 132 | 133 | /// The full name of the reference (e.g., `refs/tags/my-tag`). 134 | public var longName: String { 135 | switch self { 136 | case let .lightweight(name, _): 137 | return name 138 | case let .annotated(name, _): 139 | return name 140 | } 141 | } 142 | 143 | /// The short human-readable name of the branch (e.g., `master`). 144 | public var name: String { 145 | return String(longName["refs/tags/".endIndex...]) 146 | } 147 | 148 | /// The OID of the target object. 149 | /// 150 | /// If this is an annotated tag, the OID will be the tag's target. 151 | public var oid: OID { 152 | switch self { 153 | case let .lightweight(_, oid): 154 | return oid 155 | case let .annotated(_, tag): 156 | return tag.target.oid 157 | } 158 | } 159 | 160 | // MARK: Derived Properties 161 | 162 | /// The short human-readable name of the branch (e.g., `master`). 163 | /// 164 | /// This is the same as `name`, but is declared with an Optional type to adhere to 165 | /// `ReferenceType`. 166 | public var shortName: String? { return name } 167 | 168 | /// Create an instance with a libgit2 `git_reference` object. 169 | /// 170 | /// Returns `nil` if the pointer isn't a branch. 171 | public init?(_ pointer: OpaquePointer) { 172 | if git_reference_is_tag(pointer) == 0 { 173 | return nil 174 | } 175 | 176 | let name = String(validatingUTF8: git_reference_name(pointer))! 177 | let repo = git_reference_owner(pointer) 178 | var oid = git_reference_target(pointer).pointee 179 | 180 | var pointer: OpaquePointer? = nil 181 | let result = git_object_lookup(&pointer, repo, &oid, GIT_OBJECT_TAG) 182 | if result == GIT_OK.rawValue { 183 | self = .annotated(name, Tag(pointer!)) 184 | } else { 185 | self = .lightweight(name, OID(oid)) 186 | } 187 | git_object_free(pointer) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /Tests/GitTests/ReferencesSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReferencesSpec.swift 3 | // SwiftGit2 4 | // 5 | // Created by Matt Diephouse on 1/2/15. 6 | // Copyright (c) 2015 GitHub, Inc. All rights reserved. 7 | // 8 | 9 | import Git 10 | import Nimble 11 | import Quick 12 | import Clibgit2 13 | 14 | private extension Repository { 15 | func withGitReference(named name: String, transform: (OpaquePointer) -> T) -> T { 16 | let repository = self.pointer 17 | 18 | var pointer: OpaquePointer? = nil 19 | git_reference_lookup(&pointer, repository, name) 20 | let result = transform(pointer!) 21 | git_reference_free(pointer) 22 | 23 | return result 24 | } 25 | } 26 | 27 | class ReferenceSpec: FixturesSpec { 28 | override func spec() { 29 | describe("Reference(pointer)") { 30 | it("should initialize its properties") { 31 | let repo = Fixtures.simpleRepository 32 | let ref = repo.withGitReference(named: "refs/heads/master") { Reference($0) } 33 | expect(ref.longName).to(equal("refs/heads/master")) 34 | expect(ref.shortName).to(equal("master")) 35 | expect(ref.oid).to(equal(OID(string: "c4ed03a6b7d7ce837d31d83757febbe84dd465fd")!)) 36 | } 37 | } 38 | 39 | describe("==(Reference, Reference)") { 40 | it("should be true with equal references") { 41 | let repo = Fixtures.simpleRepository 42 | let ref1 = repo.withGitReference(named: "refs/heads/master") { Reference($0) } 43 | let ref2 = repo.withGitReference(named: "refs/heads/master") { Reference($0) } 44 | expect(ref1).to(equal(ref2)) 45 | } 46 | 47 | it("should be false with unequal references") { 48 | let repo = Fixtures.simpleRepository 49 | let ref1 = repo.withGitReference(named: "refs/heads/master") { Reference($0) } 50 | let ref2 = repo.withGitReference(named: "refs/heads/another-branch") { Reference($0) } 51 | expect(ref1).notTo(equal(ref2)) 52 | } 53 | } 54 | 55 | describe("Reference.hashValue") { 56 | it("should be equal with equal references") { 57 | let repo = Fixtures.simpleRepository 58 | let ref1 = repo.withGitReference(named: "refs/heads/master") { Reference($0) } 59 | let ref2 = repo.withGitReference(named: "refs/heads/master") { Reference($0) } 60 | expect(ref1.hashValue).to(equal(ref2.hashValue)) 61 | } 62 | } 63 | } 64 | } 65 | 66 | class BranchSpec: QuickSpec { 67 | override func spec() { 68 | describe("Branch(pointer)") { 69 | it("should initialize its properties") { 70 | let repo = Fixtures.mantleRepository 71 | let branch = repo.withGitReference(named: "refs/heads/master") { Branch($0)! } 72 | expect(branch.longName).to(equal("refs/heads/master")) 73 | expect(branch.name).to(equal("master")) 74 | expect(branch.shortName).to(equal(branch.name)) 75 | expect(branch.commit.oid).to(equal(OID(string: "f797bd4837b61d37847a4833024aab268599a681")!)) 76 | expect(branch.oid).to(equal(branch.commit.oid)) 77 | expect(branch.isLocal).to(beTrue()) 78 | expect(branch.isRemote).to(beFalse()) 79 | } 80 | 81 | it("should work with symoblic refs") { 82 | let repo = Fixtures.mantleRepository 83 | let branch = repo.withGitReference(named: "refs/remotes/origin/HEAD") { Branch($0)! } 84 | expect(branch.longName).to(equal("refs/remotes/origin/HEAD")) 85 | expect(branch.name).to(equal("origin/HEAD")) 86 | expect(branch.shortName).to(equal(branch.name)) 87 | expect(branch.commit.oid).to(equal(OID(string: "f797bd4837b61d37847a4833024aab268599a681")!)) 88 | expect(branch.oid).to(equal(branch.commit.oid)) 89 | expect(branch.isLocal).to(beFalse()) 90 | expect(branch.isRemote).to(beTrue()) 91 | } 92 | } 93 | 94 | describe("==(Branch, Branch)") { 95 | it("should be true with equal branches") { 96 | let repo = Fixtures.simpleRepository 97 | let branch1 = repo.withGitReference(named: "refs/heads/master") { Branch($0)! } 98 | let branch2 = repo.withGitReference(named: "refs/heads/master") { Branch($0)! } 99 | expect(branch1).to(equal(branch2)) 100 | } 101 | 102 | it("should be false with unequal branches") { 103 | let repo = Fixtures.simpleRepository 104 | let branch1 = repo.withGitReference(named: "refs/heads/master") { Branch($0)! } 105 | let branch2 = repo.withGitReference(named: "refs/heads/another-branch") { Branch($0)! } 106 | expect(branch1).notTo(equal(branch2)) 107 | } 108 | } 109 | 110 | describe("Branch.hashValue") { 111 | it("should be equal with equal references") { 112 | let repo = Fixtures.simpleRepository 113 | let branch1 = repo.withGitReference(named: "refs/heads/master") { Branch($0)! } 114 | let branch2 = repo.withGitReference(named: "refs/heads/master") { Branch($0)! } 115 | expect(branch1.hashValue).to(equal(branch2.hashValue)) 116 | } 117 | } 118 | } 119 | } 120 | 121 | class TagReferenceSpec: QuickSpec { 122 | override func spec() { 123 | describe("TagReference(pointer)") { 124 | it("should work with an annotated tag") { 125 | let repo = Fixtures.simpleRepository 126 | let tag = repo.withGitReference(named: "refs/tags/tag-2") { TagReference($0)! } 127 | expect(tag.longName).to(equal("refs/tags/tag-2")) 128 | expect(tag.name).to(equal("tag-2")) 129 | expect(tag.shortName).to(equal(tag.name)) 130 | expect(tag.oid).to(equal(OID(string: "24e1e40ee77525d9e279f079f9906ad6d98c8940")!)) 131 | } 132 | 133 | it("should work with a lightweight tag") { 134 | let repo = Fixtures.mantleRepository 135 | let tag = repo.withGitReference(named: "refs/tags/1.5.4") { TagReference($0)! } 136 | expect(tag.longName).to(equal("refs/tags/1.5.4")) 137 | expect(tag.name).to(equal("1.5.4")) 138 | expect(tag.shortName).to(equal(tag.name)) 139 | expect(tag.oid).to(equal(OID(string: "d9dc95002cfbf3929d2b70d2c8a77e6bf5b1b88a")!)) 140 | } 141 | 142 | it("should return nil if not a tag") { 143 | let repo = Fixtures.simpleRepository 144 | let tag = repo.withGitReference(named: "refs/heads/master") { TagReference($0) } 145 | expect(tag).to(beNil()) 146 | } 147 | } 148 | 149 | describe("==(TagReference, TagReference)") { 150 | it("should be true with equal tag references") { 151 | let repo = Fixtures.simpleRepository 152 | let tag1 = repo.withGitReference(named: "refs/tags/tag-2") { TagReference($0)! } 153 | let tag2 = repo.withGitReference(named: "refs/tags/tag-2") { TagReference($0)! } 154 | expect(tag1).to(equal(tag2)) 155 | } 156 | 157 | it("should be false with unequal tag references") { 158 | let repo = Fixtures.simpleRepository 159 | let tag1 = repo.withGitReference(named: "refs/tags/tag-1") { TagReference($0)! } 160 | let tag2 = repo.withGitReference(named: "refs/tags/tag-2") { TagReference($0)! } 161 | expect(tag1).notTo(equal(tag2)) 162 | } 163 | } 164 | 165 | describe("TagReference.hashValue") { 166 | it("should be equal with equal references") { 167 | let repo = Fixtures.simpleRepository 168 | let tag1 = repo.withGitReference(named: "refs/tags/tag-2") { TagReference($0)! } 169 | let tag2 = repo.withGitReference(named: "refs/tags/tag-2") { TagReference($0)! } 170 | expect(tag1.hashValue).to(equal(tag2.hashValue)) 171 | } 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /Sources/Git/Objects.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Objects.swift 3 | // SwiftGit2 4 | // 5 | // Created by Matt Diephouse on 12/4/14. 6 | // Copyright (c) 2014 GitHub, Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Clibgit2 11 | 12 | /// A git object. 13 | public protocol ObjectType { 14 | static var type: git_object_t { get } 15 | 16 | /// The OID of the object. 17 | var oid: OID { get } 18 | 19 | /// Create an instance with the underlying libgit2 type. 20 | init(_ pointer: OpaquePointer) 21 | } 22 | 23 | public extension ObjectType { 24 | static func == (lhs: Self, rhs: Self) -> Bool { 25 | return lhs.oid == rhs.oid 26 | } 27 | 28 | func hash(into hasher: inout Hasher) { 29 | hasher.combine(oid) 30 | } 31 | } 32 | 33 | public struct Signature { 34 | /// The name of the person. 35 | public let name: String 36 | 37 | /// The email of the person. 38 | public let email: String 39 | 40 | /// The time when the action happened. 41 | public let time: Date 42 | 43 | /// The time zone that `time` should be interpreted relative to. 44 | public let timeZone: TimeZone 45 | 46 | /// Create an instance with custom name, email, dates, etc. 47 | public init(name: String, email: String, time: Date = Date(), timeZone: TimeZone = TimeZone.autoupdatingCurrent) { 48 | self.name = name 49 | self.email = email 50 | self.time = time 51 | self.timeZone = timeZone 52 | } 53 | 54 | /// Create an instance with a libgit2 `git_signature`. 55 | public init(_ signature: git_signature) { 56 | name = String(validatingUTF8: signature.name)! 57 | email = String(validatingUTF8: signature.email)! 58 | time = Date(timeIntervalSince1970: TimeInterval(signature.when.time)) 59 | timeZone = TimeZone(secondsFromGMT: 60 * Int(signature.when.offset))! 60 | } 61 | 62 | /// Return an unsafe pointer to the `git_signature` struct. 63 | /// Caller is responsible for freeing it with `git_signature_free`. 64 | func makeUnsafeSignature() -> Result, NSError> { 65 | var signature: UnsafeMutablePointer? = nil 66 | let time = git_time_t(self.time.timeIntervalSince1970) // Unix epoch time 67 | let offset = Int32(timeZone.secondsFromGMT(for: self.time) / 60) 68 | let signatureResult = git_signature_new(&signature, name, email, time, offset) 69 | guard signatureResult == GIT_OK.rawValue, let signatureUnwrap = signature else { 70 | let err = NSError(gitError: signatureResult, pointOfFailure: "git_signature_new") 71 | return .failure(err) 72 | } 73 | return .success(signatureUnwrap) 74 | } 75 | } 76 | 77 | extension Signature: Hashable { 78 | public func hash(into hasher: inout Hasher) { 79 | hasher.combine(name) 80 | hasher.combine(email) 81 | hasher.combine(time) 82 | } 83 | } 84 | 85 | /// A git commit. 86 | public struct Commit: ObjectType, Hashable { 87 | public static let type = GIT_OBJECT_COMMIT 88 | 89 | /// The OID of the commit. 90 | public let oid: OID 91 | 92 | /// The OID of the commit's tree. 93 | public let tree: PointerTo 94 | 95 | /// The OIDs of the commit's parents. 96 | public let parents: [PointerTo] 97 | 98 | /// The author of the commit. 99 | public let author: Signature 100 | 101 | /// The committer of the commit. 102 | public let committer: Signature 103 | 104 | /// The full message of the commit. 105 | public let message: String 106 | 107 | /// Create an instance with a libgit2 `git_commit` object. 108 | public init(_ pointer: OpaquePointer) { 109 | oid = OID(git_object_id(pointer).pointee) 110 | message = String(validatingUTF8: git_commit_message(pointer))! 111 | author = Signature(git_commit_author(pointer).pointee) 112 | committer = Signature(git_commit_committer(pointer).pointee) 113 | tree = PointerTo(OID(git_commit_tree_id(pointer).pointee)) 114 | 115 | self.parents = (0..(_ oid: OID, transform: (OpaquePointer) -> T) -> T { 17 | let repository = self.pointer 18 | var oid = oid.oid 19 | 20 | var pointer: OpaquePointer? = nil 21 | git_object_lookup(&pointer, repository, &oid, GIT_OBJECT_ANY) 22 | let result = transform(pointer!) 23 | git_object_free(pointer) 24 | 25 | return result 26 | } 27 | } 28 | 29 | class SignatureSpec: FixturesSpec { 30 | override func spec() { 31 | describe("Signature(signature)") { 32 | it("should initialize its properties") { 33 | let repo = Fixtures.simpleRepository 34 | let oid = OID(string: "dc220a3f0c22920dab86d4a8d3a3cb7e69d6205a")! 35 | 36 | let raw_signature = repo.withGitObject(oid) { git_commit_author($0).pointee } 37 | let signature = Signature(raw_signature) 38 | 39 | expect(signature.name).to(equal("Matt Diephouse")) 40 | expect(signature.email).to(equal("matt@diephouse.com")) 41 | expect(signature.time).to(equal(Date(timeIntervalSince1970: 1416186947))) 42 | expect(signature.timeZone.abbreviation()).to(equal("GMT-0500")) 43 | } 44 | } 45 | 46 | describe("==(Signature, Signature)") { 47 | it("should be true with equal objects") { 48 | let repo = Fixtures.simpleRepository 49 | let oid = OID(string: "dc220a3f0c22920dab86d4a8d3a3cb7e69d6205a")! 50 | 51 | let author1 = repo.withGitObject(oid) { commit in 52 | Signature(git_commit_author(commit).pointee) 53 | } 54 | let author2 = author1 55 | 56 | expect(author1).to(equal(author2)) 57 | } 58 | 59 | it("should be false with unequal objects") { 60 | let repo = Fixtures.simpleRepository 61 | let oid1 = OID(string: "dc220a3f0c22920dab86d4a8d3a3cb7e69d6205a")! 62 | let oid2 = OID(string: "24e1e40ee77525d9e279f079f9906ad6d98c8940")! 63 | 64 | let author1 = repo.withGitObject(oid1) { commit in 65 | Signature(git_commit_author(commit).pointee) 66 | } 67 | let author2 = repo.withGitObject(oid2) { commit in 68 | Signature(git_commit_author(commit).pointee) 69 | } 70 | 71 | expect(author1).notTo(equal(author2)) 72 | } 73 | } 74 | 75 | describe("Signature.hashValue") { 76 | it("should be equal with equal objects") { 77 | let repo = Fixtures.simpleRepository 78 | let oid = OID(string: "dc220a3f0c22920dab86d4a8d3a3cb7e69d6205a")! 79 | 80 | let author1 = repo.withGitObject(oid) { commit in 81 | Signature(git_commit_author(commit).pointee) 82 | } 83 | let author2 = author1 84 | 85 | expect(author1.hashValue).to(equal(author2.hashValue)) 86 | } 87 | } 88 | } 89 | } 90 | 91 | class CommitSpec: QuickSpec { 92 | override func spec() { 93 | describe("Commit(pointer)") { 94 | it("should initialize its properties") { 95 | let repo = Fixtures.simpleRepository 96 | let oid = OID(string: "24e1e40ee77525d9e279f079f9906ad6d98c8940")! 97 | 98 | let commit = repo.withGitObject(oid) { Commit($0) } 99 | let author = repo.withGitObject(oid) { commit in 100 | Signature(git_commit_author(commit).pointee) 101 | } 102 | let committer = repo.withGitObject(oid) { commit in 103 | Signature(git_commit_committer(commit).pointee) 104 | } 105 | let tree = PointerTo(OID(string: "219e9f39c2fb59ed1dfb3e78ed75055a57528f31")!) 106 | let parents: [PointerTo] = [ 107 | PointerTo(OID(string: "dc220a3f0c22920dab86d4a8d3a3cb7e69d6205a")!), 108 | ] 109 | expect(commit.oid).to(equal(oid)) 110 | expect(commit.tree).to(equal(tree)) 111 | expect(commit.parents).to(equal(parents)) 112 | expect(commit.message).to(equal("List branches in README\n")) 113 | expect(commit.author).to(equal(author)) 114 | expect(commit.committer).to(equal(committer)) 115 | } 116 | 117 | it("should handle 0 parents") { 118 | let repo = Fixtures.simpleRepository 119 | let oid = OID(string: "dc220a3f0c22920dab86d4a8d3a3cb7e69d6205a")! 120 | 121 | let commit = repo.withGitObject(oid) { Commit($0) } 122 | expect(commit.parents).to(equal([])) 123 | } 124 | 125 | it("should handle multiple parents") { 126 | let repo = Fixtures.simpleRepository 127 | let oid = OID(string: "c4ed03a6b7d7ce837d31d83757febbe84dd465fd")! 128 | 129 | let commit = repo.withGitObject(oid) { Commit($0) } 130 | let parents: [PointerTo] = [ 131 | PointerTo(OID(string: "315b3f344221db91ddc54b269f3c9af422da0f2e")!), 132 | PointerTo(OID(string: "57f6197561d1f99b03c160f4026a07f06b43cf20")!), 133 | ] 134 | expect(commit.parents).to(equal(parents)) 135 | } 136 | } 137 | 138 | describe("==(Commit, Commit)") { 139 | it("should be true with equal objects") { 140 | let repo = Fixtures.simpleRepository 141 | let oid = OID(string: "dc220a3f0c22920dab86d4a8d3a3cb7e69d6205a")! 142 | 143 | let commit1 = repo.withGitObject(oid) { Commit($0) } 144 | let commit2 = commit1 145 | expect(commit1).to(equal(commit2)) 146 | } 147 | 148 | it("should be false with unequal objects") { 149 | let repo = Fixtures.simpleRepository 150 | let oid1 = OID(string: "dc220a3f0c22920dab86d4a8d3a3cb7e69d6205a")! 151 | let oid2 = OID(string: "c4ed03a6b7d7ce837d31d83757febbe84dd465fd")! 152 | 153 | let commit1 = repo.withGitObject(oid1) { Commit($0) } 154 | let commit2 = repo.withGitObject(oid2) { Commit($0) } 155 | expect(commit1).notTo(equal(commit2)) 156 | } 157 | } 158 | 159 | describe("Commit.hashValue") { 160 | it("should be equal with equal objects") { 161 | let repo = Fixtures.simpleRepository 162 | let oid = OID(string: "dc220a3f0c22920dab86d4a8d3a3cb7e69d6205a")! 163 | 164 | let commit1 = repo.withGitObject(oid) { Commit($0) } 165 | let commit2 = commit1 166 | expect(commit1.hashValue).to(equal(commit2.hashValue)) 167 | } 168 | } 169 | } 170 | } 171 | 172 | class TreeEntrySpec: QuickSpec { 173 | override func spec() { 174 | describe("Tree.Entry(attributes:object:name:)") { 175 | it("should set its properties") { 176 | let attributes = Int32(GIT_FILEMODE_BLOB.rawValue) 177 | let object = Pointer.blob(OID(string: "41078396f5187daed5f673e4a13b185bbad71fba")!) 178 | let name = "README.md" 179 | 180 | let entry = Tree.Entry(attributes: attributes, object: object, name: name) 181 | expect(entry.attributes).to(equal(attributes)) 182 | expect(entry.object).to(equal(object)) 183 | expect(entry.name).to(equal(name)) 184 | } 185 | } 186 | 187 | describe("Tree.Entry(pointer)") { 188 | it("should set its properties") { 189 | let repo = Fixtures.simpleRepository 190 | let oid = OID(string: "219e9f39c2fb59ed1dfb3e78ed75055a57528f31")! 191 | 192 | let entry = repo.withGitObject(oid) { Tree.Entry(git_tree_entry_byindex($0, 0)) } 193 | expect(entry.attributes).to(equal(Int32(GIT_FILEMODE_BLOB.rawValue))) 194 | expect(entry.object).to(equal(Pointer.blob(OID(string: "41078396f5187daed5f673e4a13b185bbad71fba")!))) 195 | expect(entry.name).to(equal("README.md")) 196 | } 197 | } 198 | 199 | describe("==(Tree.Entry, Tree.Entry)") { 200 | it("should be true with equal objects") { 201 | let repo = Fixtures.simpleRepository 202 | let oid = OID(string: "219e9f39c2fb59ed1dfb3e78ed75055a57528f31")! 203 | 204 | let entry1 = repo.withGitObject(oid) { Tree.Entry(git_tree_entry_byindex($0, 0)) } 205 | let entry2 = entry1 206 | expect(entry1).to(equal(entry2)) 207 | } 208 | 209 | it("should be false with unequal objects") { 210 | let repo = Fixtures.simpleRepository 211 | let oid1 = OID(string: "219e9f39c2fb59ed1dfb3e78ed75055a57528f31")! 212 | let oid2 = OID(string: "f93e3a1a1525fb5b91020da86e44810c87a2d7bc")! 213 | 214 | let entry1 = repo.withGitObject(oid1) { Tree.Entry(git_tree_entry_byindex($0, 0)) } 215 | let entry2 = repo.withGitObject(oid2) { Tree.Entry(git_tree_entry_byindex($0, 0)) } 216 | expect(entry1).notTo(equal(entry2)) 217 | } 218 | } 219 | 220 | describe("Tree.Entry.hashValue") { 221 | it("should be equal with equal objects") { 222 | let repo = Fixtures.simpleRepository 223 | let oid = OID(string: "219e9f39c2fb59ed1dfb3e78ed75055a57528f31")! 224 | 225 | let entry1 = repo.withGitObject(oid) { Tree.Entry(git_tree_entry_byindex($0, 0)) } 226 | let entry2 = entry1 227 | expect(entry1.hashValue).to(equal(entry2.hashValue)) 228 | } 229 | } 230 | } 231 | } 232 | 233 | class TreeSpec: QuickSpec { 234 | override func spec() { 235 | describe("Tree(pointer)") { 236 | it("should initialize its properties") { 237 | let repo = Fixtures.simpleRepository 238 | let oid = OID(string: "219e9f39c2fb59ed1dfb3e78ed75055a57528f31")! 239 | 240 | let tree = repo.withGitObject(oid) { Tree($0) } 241 | let entries = [ 242 | "README.md": Tree.Entry(attributes: Int32(GIT_FILEMODE_BLOB.rawValue), 243 | object: .blob(OID(string: "41078396f5187daed5f673e4a13b185bbad71fba")!), 244 | name: "README.md"), 245 | ] 246 | expect(tree.entries).to(equal(entries)) 247 | } 248 | } 249 | 250 | describe("==(Tree, Tree)") { 251 | it("should be true with equal objects") { 252 | let repo = Fixtures.simpleRepository 253 | let oid = OID(string: "219e9f39c2fb59ed1dfb3e78ed75055a57528f31")! 254 | 255 | let tree1 = repo.withGitObject(oid) { Tree($0) } 256 | let tree2 = tree1 257 | expect(tree1).to(equal(tree2)) 258 | } 259 | 260 | it("should be false with unequal objects") { 261 | let repo = Fixtures.simpleRepository 262 | let oid1 = OID(string: "219e9f39c2fb59ed1dfb3e78ed75055a57528f31")! 263 | let oid2 = OID(string: "f93e3a1a1525fb5b91020da86e44810c87a2d7bc")! 264 | 265 | let tree1 = repo.withGitObject(oid1) { Tree($0) } 266 | let tree2 = repo.withGitObject(oid2) { Tree($0) } 267 | expect(tree1).notTo(equal(tree2)) 268 | } 269 | } 270 | 271 | describe("Tree.hashValue") { 272 | it("should be equal with equal objects") { 273 | let repo = Fixtures.simpleRepository 274 | let oid = OID(string: "219e9f39c2fb59ed1dfb3e78ed75055a57528f31")! 275 | 276 | let tree1 = repo.withGitObject(oid) { Tree($0) } 277 | let tree2 = tree1 278 | expect(tree1.hashValue).to(equal(tree2.hashValue)) 279 | } 280 | } 281 | } 282 | } 283 | 284 | class BlobSpec: QuickSpec { 285 | override func spec() { 286 | describe("Blob(pointer)") { 287 | it("should initialize its properties") { 288 | let repo = Fixtures.simpleRepository 289 | let oid = OID(string: "41078396f5187daed5f673e4a13b185bbad71fba")! 290 | 291 | let blob = repo.withGitObject(oid) { Blob($0) } 292 | let contents = "# Simple Repository\nA simple repository used for testing SwiftGit2.\n\n## Branches\n\n- master\n\n" 293 | let data = contents.data(using: String.Encoding.utf8)! 294 | expect(blob.oid).to(equal(oid)) 295 | expect(blob.data).to(equal(data)) 296 | } 297 | } 298 | 299 | describe("==(Blob, Blob)") { 300 | it("should be true with equal objects") { 301 | let repo = Fixtures.simpleRepository 302 | let oid = OID(string: "41078396f5187daed5f673e4a13b185bbad71fba")! 303 | 304 | let blob1 = repo.withGitObject(oid) { Blob($0) } 305 | let blob2 = blob1 306 | expect(blob1).to(equal(blob2)) 307 | } 308 | 309 | it("should be false with unequal objects") { 310 | let repo = Fixtures.simpleRepository 311 | let oid1 = OID(string: "41078396f5187daed5f673e4a13b185bbad71fba")! 312 | let oid2 = OID(string: "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391")! 313 | 314 | let blob1 = repo.withGitObject(oid1) { Blob($0) } 315 | let blob2 = repo.withGitObject(oid2) { Blob($0) } 316 | expect(blob1).notTo(equal(blob2)) 317 | } 318 | } 319 | 320 | describe("Blob.hashValue") { 321 | it("should be equal with equal objects") { 322 | let repo = Fixtures.simpleRepository 323 | let oid = OID(string: "41078396f5187daed5f673e4a13b185bbad71fba")! 324 | 325 | let blob1 = repo.withGitObject(oid) { Blob($0) } 326 | let blob2 = blob1 327 | expect(blob1.hashValue).to(equal(blob2.hashValue)) 328 | } 329 | } 330 | } 331 | } 332 | 333 | class TagSpec: QuickSpec { 334 | override func spec() { 335 | describe("Tag(pointer)") { 336 | it("should set its properties") { 337 | let repo = Fixtures.simpleRepository 338 | let oid = OID(string: "57943b8ee00348180ceeedc960451562750f6d33")! 339 | 340 | let tag = repo.withGitObject(oid) { Tag($0) } 341 | let tagger = repo.withGitObject(oid) { Signature(git_tag_tagger($0).pointee) } 342 | 343 | expect(tag.oid).to(equal(oid)) 344 | expect(tag.target).to(equal(Pointer.commit(OID(string: "dc220a3f0c22920dab86d4a8d3a3cb7e69d6205a")!))) 345 | expect(tag.name).to(equal("tag-1")) 346 | expect(tag.tagger).to(equal(tagger)) 347 | expect(tag.message).to(equal("tag-1\n")) 348 | } 349 | } 350 | 351 | describe("==(Tag, Tag)") { 352 | it("should be true with equal objects") { 353 | let repo = Fixtures.simpleRepository 354 | let oid = OID(string: "57943b8ee00348180ceeedc960451562750f6d33")! 355 | 356 | let tag1 = repo.withGitObject(oid) { Tag($0) } 357 | let tag2 = tag1 358 | expect(tag1).to(equal(tag2)) 359 | } 360 | 361 | it("should be false with unequal objects") { 362 | let repo = Fixtures.simpleRepository 363 | let oid1 = OID(string: "57943b8ee00348180ceeedc960451562750f6d33")! 364 | let oid2 = OID(string: "13bda91157f255ab224ff88d0a11a82041c9d0c1")! 365 | 366 | let tag1 = repo.withGitObject(oid1) { Tag($0) } 367 | let tag2 = repo.withGitObject(oid2) { Tag($0) } 368 | expect(tag1).notTo(equal(tag2)) 369 | } 370 | } 371 | 372 | describe("Tag.hashValue") { 373 | it("should be equal with equal objects") { 374 | let repo = Fixtures.simpleRepository 375 | let oid = OID(string: "57943b8ee00348180ceeedc960451562750f6d33")! 376 | 377 | let tag1 = repo.withGitObject(oid) { Tag($0) } 378 | let tag2 = tag1 379 | expect(tag1.hashValue).to(equal(tag2.hashValue)) 380 | } 381 | } 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /Sources/Git/Repository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Repository.swift 3 | // SwiftGit2 4 | // 5 | // Created by Matt Diephouse on 11/7/14. 6 | // Copyright (c) 2014 GitHub, Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Clibgit2 11 | 12 | public typealias CheckoutProgressBlock = (String?, Int, Int) -> Void 13 | 14 | /// Helper function used as the libgit2 progress callback in git_checkout_options. 15 | /// This is a function with a type signature of git_checkout_progress_cb. 16 | private func checkoutProgressCallback(path: UnsafePointer?, completedSteps: Int, totalSteps: Int, 17 | payload: UnsafeMutableRawPointer?) { 18 | if let payload = payload { 19 | let buffer = payload.assumingMemoryBound(to: CheckoutProgressBlock.self) 20 | let block: CheckoutProgressBlock 21 | if completedSteps < totalSteps { 22 | block = buffer.pointee 23 | } else { 24 | block = buffer.move() 25 | buffer.deallocate() 26 | } 27 | block(path.flatMap(String.init(validatingUTF8:)), completedSteps, totalSteps) 28 | } 29 | } 30 | 31 | /// Helper function for initializing libgit2 git_checkout_options. 32 | /// 33 | /// :param: strategy The strategy to be used when checking out the repo, see CheckoutStrategy 34 | /// :param: progress A block that's called with the progress of the checkout. 35 | /// :returns: Returns a git_checkout_options struct with the progress members set. 36 | private func checkoutOptions(strategy: CheckoutStrategy, 37 | progress: CheckoutProgressBlock? = nil) -> git_checkout_options { 38 | // Do this because GIT_CHECKOUT_OPTIONS_INIT is unavailable in swift 39 | let pointer = UnsafeMutablePointer.allocate(capacity: 1) 40 | git_checkout_init_options(pointer, UInt32(GIT_CHECKOUT_OPTIONS_VERSION)) 41 | var options = pointer.move() 42 | pointer.deallocate() 43 | 44 | options.checkout_strategy = strategy.gitCheckoutStrategy.rawValue 45 | 46 | if progress != nil { 47 | options.progress_cb = checkoutProgressCallback 48 | let blockPointer = UnsafeMutablePointer.allocate(capacity: 1) 49 | blockPointer.initialize(to: progress!) 50 | options.progress_payload = UnsafeMutableRawPointer(blockPointer) 51 | } 52 | 53 | return options 54 | } 55 | 56 | private func fetchOptions(credentials: Credentials) -> git_fetch_options { 57 | let pointer = UnsafeMutablePointer.allocate(capacity: 1) 58 | git_fetch_init_options(pointer, UInt32(GIT_FETCH_OPTIONS_VERSION)) 59 | 60 | var options = pointer.move() 61 | 62 | pointer.deallocate() 63 | 64 | options.callbacks.payload = credentials.toPointer() 65 | options.callbacks.credentials = credentialsCallback 66 | 67 | return options 68 | } 69 | 70 | private func cloneOptions(bare: Bool = false, localClone: Bool = false, fetchOptions: git_fetch_options? = nil, 71 | checkoutOptions: git_checkout_options? = nil) -> git_clone_options { 72 | let pointer = UnsafeMutablePointer.allocate(capacity: 1) 73 | git_clone_init_options(pointer, UInt32(GIT_CLONE_OPTIONS_VERSION)) 74 | 75 | var options = pointer.move() 76 | 77 | pointer.deallocate() 78 | 79 | options.bare = bare ? 1 : 0 80 | 81 | if localClone { 82 | options.local = GIT_CLONE_NO_LOCAL 83 | } 84 | 85 | if let checkoutOptions = checkoutOptions { 86 | options.checkout_opts = checkoutOptions 87 | } 88 | 89 | if let fetchOptions = fetchOptions { 90 | options.fetch_opts = fetchOptions 91 | } 92 | 93 | return options 94 | } 95 | 96 | /// A git repository. 97 | public final class Repository { 98 | /// Only used for running `git_libgit2_init()` exactly once. 99 | private static var gitInit: Void = { git_libgit2_init(); return }() 100 | 101 | // MARK: - Creating Repositories 102 | 103 | /// Create a new repository at the given URL. 104 | /// 105 | /// URL - The URL of the repository. 106 | /// 107 | /// Returns a `Result` with a `Repository` or an error. 108 | public class func create(at url: URL) -> Result { 109 | _ = Self.gitInit 110 | var pointer: OpaquePointer? = nil 111 | let result = url.withUnsafeFileSystemRepresentation { 112 | git_repository_init(&pointer, $0, 0) 113 | } 114 | 115 | guard result == GIT_OK.rawValue else { 116 | return Result.failure(NSError(gitError: result, pointOfFailure: "git_repository_init")) 117 | } 118 | 119 | let repository = Repository(pointer!) 120 | return Result.success(repository) 121 | } 122 | 123 | /// Load the repository at the given URL. 124 | /// 125 | /// URL - The URL of the repository. 126 | /// 127 | /// Returns a `Result` with a `Repository` or an error. 128 | public class func at(_ url: URL) -> Result { 129 | _ = Self.gitInit 130 | var pointer: OpaquePointer? = nil 131 | let result = url.withUnsafeFileSystemRepresentation { 132 | git_repository_open(&pointer, $0) 133 | } 134 | 135 | guard result == GIT_OK.rawValue else { 136 | return Result.failure(NSError(gitError: result, pointOfFailure: "git_repository_open")) 137 | } 138 | 139 | let repository = Repository(pointer!) 140 | return Result.success(repository) 141 | } 142 | 143 | /// Clone the repository from a given URL. 144 | /// 145 | /// remoteURL - The URL of the remote repository 146 | /// localURL - The URL to clone the remote repository into 147 | /// localClone - Will not bypass the git-aware transport, even if remote is local. 148 | /// bare - Clone remote as a bare repository. 149 | /// credentials - Credentials to be used when connecting to the remote. 150 | /// checkoutStrategy - The checkout strategy to use, if being checked out. 151 | /// checkoutProgress - A block that's called with the progress of the checkout. 152 | /// 153 | /// Returns a `Result` with a `Repository` or an error. 154 | public class func clone(from remoteURL: URL, to localURL: URL, localClone: Bool = false, bare: Bool = false, 155 | credentials: Credentials = .default, checkoutStrategy: CheckoutStrategy = .Safe, 156 | checkoutProgress: CheckoutProgressBlock? = nil) -> Result { 157 | _ = Self.gitInit 158 | var options = cloneOptions( 159 | bare: bare, 160 | localClone: localClone, 161 | fetchOptions: fetchOptions(credentials: credentials), 162 | checkoutOptions: checkoutOptions(strategy: checkoutStrategy, progress: checkoutProgress)) 163 | 164 | var pointer: OpaquePointer? = nil 165 | let remoteURLString = remoteURL.isFileURL ? remoteURL.path : remoteURL.absoluteString 166 | let result = localURL.withUnsafeFileSystemRepresentation { localPath in 167 | git_clone(&pointer, remoteURLString, localPath, &options) 168 | } 169 | 170 | guard result == GIT_OK.rawValue else { 171 | return Result.failure(NSError(gitError: result, pointOfFailure: "git_clone")) 172 | } 173 | 174 | let repository = Repository(pointer!) 175 | return Result.success(repository) 176 | } 177 | 178 | // MARK: - Initializers 179 | 180 | /// Create an instance with a libgit2 `git_repository` object. 181 | /// 182 | /// The Repository assumes ownership of the `git_repository` object. 183 | public init(_ pointer: OpaquePointer) { 184 | self.pointer = pointer 185 | 186 | let path = git_repository_workdir(pointer) 187 | self.directoryURL = path.map({ URL(fileURLWithPath: String(validatingUTF8: $0)!, isDirectory: true) }) 188 | } 189 | 190 | deinit { 191 | git_repository_free(pointer) 192 | } 193 | 194 | // MARK: - Properties 195 | 196 | /// The underlying libgit2 `git_repository` object. 197 | public let pointer: OpaquePointer 198 | 199 | /// The URL of the repository's working directory, or `nil` if the 200 | /// repository is bare. 201 | public let directoryURL: URL? 202 | 203 | // MARK: - Object Lookups 204 | 205 | /// Load a libgit2 object and transform it to something else. 206 | /// 207 | /// oid - The OID of the object to look up. 208 | /// type - The type of the object to look up. 209 | /// transform - A function that takes the libgit2 object and transforms it 210 | /// into something else. 211 | /// 212 | /// Returns the result of calling `transform` or an error if the object 213 | /// cannot be loaded. 214 | private func withGitObject(_ oid: OID, type: git_object_t, 215 | transform: (OpaquePointer) -> Result) -> Result { 216 | var pointer: OpaquePointer? = nil 217 | var oid = oid.oid 218 | let result = git_object_lookup(&pointer, self.pointer, &oid, type) 219 | 220 | guard result == GIT_OK.rawValue else { 221 | return Result.failure(NSError(gitError: result, pointOfFailure: "git_object_lookup")) 222 | } 223 | 224 | let value = transform(pointer!) 225 | git_object_free(pointer) 226 | return value 227 | } 228 | 229 | private func withGitObject(_ oid: OID, type: git_object_t, transform: (OpaquePointer) -> T) -> Result { 230 | return withGitObject(oid, type: type) { Result.success(transform($0)) } 231 | } 232 | 233 | private func withGitObjects(_ oids: [OID], type: git_object_t, transform: ([OpaquePointer]) -> Result) -> Result { 234 | var pointers = [OpaquePointer]() 235 | defer { 236 | for pointer in pointers { 237 | git_object_free(pointer) 238 | } 239 | } 240 | 241 | for oid in oids { 242 | var pointer: OpaquePointer? = nil 243 | var oid = oid.oid 244 | let result = git_object_lookup(&pointer, self.pointer, &oid, type) 245 | 246 | guard result == GIT_OK.rawValue else { 247 | return Result.failure(NSError(gitError: result, pointOfFailure: "git_object_lookup")) 248 | } 249 | 250 | pointers.append(pointer!) 251 | } 252 | 253 | return transform(pointers) 254 | } 255 | 256 | /// Loads the object with the given OID. 257 | /// 258 | /// oid - The OID of the blob to look up. 259 | /// 260 | /// Returns a `Blob`, `Commit`, `Tag`, or `Tree` if one exists, or an error. 261 | public func object(_ oid: OID) -> Result { 262 | return withGitObject(oid, type: GIT_OBJECT_ANY) { object in 263 | let type = git_object_type(object) 264 | if type == Blob.type { 265 | return Result.success(Blob(object)) 266 | } else if type == Commit.type { 267 | return Result.success(Commit(object)) 268 | } else if type == Tag.type { 269 | return Result.success(Tag(object)) 270 | } else if type == Tree.type { 271 | return Result.success(Tree(object)) 272 | } 273 | 274 | let error = NSError( 275 | domain: "org.libgit2.SwiftGit2", 276 | code: 1, 277 | userInfo: [ 278 | NSLocalizedDescriptionKey: "Unrecognized git_object_t '\(type)' for oid '\(oid)'.", 279 | ] 280 | ) 281 | return Result.failure(error) 282 | } 283 | } 284 | 285 | /// Loads the blob with the given OID. 286 | /// 287 | /// oid - The OID of the blob to look up. 288 | /// 289 | /// Returns the blob if it exists, or an error. 290 | public func blob(_ oid: OID) -> Result { 291 | return withGitObject(oid, type: GIT_OBJECT_BLOB) { Blob($0) } 292 | } 293 | 294 | /// Loads the commit with the given OID. 295 | /// 296 | /// oid - The OID of the commit to look up. 297 | /// 298 | /// Returns the commit if it exists, or an error. 299 | public func commit(_ oid: OID) -> Result { 300 | return withGitObject(oid, type: GIT_OBJECT_COMMIT) { Commit($0) } 301 | } 302 | 303 | /// Loads the tag with the given OID. 304 | /// 305 | /// oid - The OID of the tag to look up. 306 | /// 307 | /// Returns the tag if it exists, or an error. 308 | public func tag(_ oid: OID) -> Result { 309 | return withGitObject(oid, type: GIT_OBJECT_TAG) { Tag($0) } 310 | } 311 | 312 | /// Loads the tree with the given OID. 313 | /// 314 | /// oid - The OID of the tree to look up. 315 | /// 316 | /// Returns the tree if it exists, or an error. 317 | public func tree(_ oid: OID) -> Result { 318 | return withGitObject(oid, type: GIT_OBJECT_TREE) { Tree($0) } 319 | } 320 | 321 | /// Loads the referenced object from the pointer. 322 | /// 323 | /// pointer - A pointer to an object. 324 | /// 325 | /// Returns the object if it exists, or an error. 326 | public func object(from pointer: PointerTo) -> Result { 327 | return withGitObject(pointer.oid, type: pointer.type) { T($0) } 328 | } 329 | 330 | /// Loads the referenced object from the pointer. 331 | /// 332 | /// pointer - A pointer to an object. 333 | /// 334 | /// Returns the object if it exists, or an error. 335 | public func object(from pointer: Pointer) -> Result { 336 | switch pointer { 337 | case let .blob(oid): 338 | return blob(oid).map { $0 as ObjectType } 339 | case let .commit(oid): 340 | return commit(oid).map { $0 as ObjectType } 341 | case let .tag(oid): 342 | return tag(oid).map { $0 as ObjectType } 343 | case let .tree(oid): 344 | return tree(oid).map { $0 as ObjectType } 345 | } 346 | } 347 | 348 | // MARK: - Remote Lookups 349 | 350 | /// Loads all the remotes in the repository. 351 | /// 352 | /// Returns an array of remotes, or an error. 353 | public func allRemotes() -> Result<[Remote], NSError> { 354 | let pointer = UnsafeMutablePointer.allocate(capacity: 1) 355 | let result = git_remote_list(pointer, self.pointer) 356 | 357 | guard result == GIT_OK.rawValue else { 358 | pointer.deallocate() 359 | return Result.failure(NSError(gitError: result, pointOfFailure: "git_remote_list")) 360 | } 361 | 362 | let strarray = pointer.pointee 363 | let remotes: [Result] = strarray.map { 364 | return self.remote(named: $0) 365 | } 366 | git_strarray_free(pointer) 367 | pointer.deallocate() 368 | 369 | return remotes.aggregateResult() 370 | } 371 | 372 | private func remoteLookup(named name: String, _ callback: (Result) -> A) -> A { 373 | var pointer: OpaquePointer? = nil 374 | defer { git_remote_free(pointer) } 375 | 376 | let result = git_remote_lookup(&pointer, self.pointer, name) 377 | 378 | guard result == GIT_OK.rawValue else { 379 | return callback(.failure(NSError(gitError: result, pointOfFailure: "git_remote_lookup"))) 380 | } 381 | 382 | return callback(.success(pointer!)) 383 | } 384 | 385 | /// Load a remote from the repository. 386 | /// 387 | /// name - The name of the remote. 388 | /// 389 | /// Returns the remote if it exists, or an error. 390 | public func remote(named name: String) -> Result { 391 | return remoteLookup(named: name) { $0.map(Remote.init) } 392 | } 393 | 394 | /// Download new data and update tips 395 | public func fetch(_ remote: Remote) -> Result<(), NSError> { 396 | return remoteLookup(named: remote.name) { remote in 397 | remote.flatMap { pointer in 398 | var opts = git_fetch_options() 399 | let resultInit = git_fetch_init_options(&opts, UInt32(GIT_FETCH_OPTIONS_VERSION)) 400 | assert(resultInit == GIT_OK.rawValue) 401 | 402 | let result = git_remote_fetch(pointer, nil, &opts, nil) 403 | guard result == GIT_OK.rawValue else { 404 | let err = NSError(gitError: result, pointOfFailure: "git_remote_fetch") 405 | return .failure(err) 406 | } 407 | return .success(()) 408 | } 409 | } 410 | } 411 | 412 | // MARK: - Reference Lookups 413 | 414 | /// Load all the references with the given prefix (e.g. "refs/heads/") 415 | public func references(withPrefix prefix: String) -> Result<[ReferenceType], NSError> { 416 | let pointer = UnsafeMutablePointer.allocate(capacity: 1) 417 | let result = git_reference_list(pointer, self.pointer) 418 | 419 | guard result == GIT_OK.rawValue else { 420 | pointer.deallocate() 421 | return Result.failure(NSError(gitError: result, pointOfFailure: "git_reference_list")) 422 | } 423 | 424 | let strarray = pointer.pointee 425 | let references = strarray 426 | .filter { 427 | $0.hasPrefix(prefix) 428 | } 429 | .map { 430 | self.reference(named: $0) 431 | } 432 | git_strarray_free(pointer) 433 | pointer.deallocate() 434 | 435 | return references.aggregateResult() 436 | } 437 | 438 | /// Load the reference with the given long name (e.g. "refs/heads/master") 439 | /// 440 | /// If the reference is a branch, a `Branch` will be returned. If the 441 | /// reference is a tag, a `TagReference` will be returned. Otherwise, a 442 | /// `Reference` will be returned. 443 | public func reference(named name: String) -> Result { 444 | var pointer: OpaquePointer? = nil 445 | let result = git_reference_lookup(&pointer, self.pointer, name) 446 | 447 | guard result == GIT_OK.rawValue else { 448 | return Result.failure(NSError(gitError: result, pointOfFailure: "git_reference_lookup")) 449 | } 450 | 451 | let value = referenceWithLibGit2Reference(pointer!) 452 | git_reference_free(pointer) 453 | return Result.success(value) 454 | } 455 | 456 | /// Load and return a list of all local branches. 457 | public func localBranches() -> Result<[Branch], NSError> { 458 | return references(withPrefix: "refs/heads/") 459 | .map { (refs: [ReferenceType]) in 460 | return refs.map { $0 as! Branch } 461 | } 462 | } 463 | 464 | /// Load and return a list of all remote branches. 465 | public func remoteBranches() -> Result<[Branch], NSError> { 466 | return references(withPrefix: "refs/remotes/") 467 | .map { (refs: [ReferenceType]) in 468 | return refs.map { $0 as! Branch } 469 | } 470 | } 471 | 472 | /// Load the local branch with the given name (e.g., "master"). 473 | public func localBranch(named name: String) -> Result { 474 | return reference(named: "refs/heads/" + name).map { $0 as! Branch } 475 | } 476 | 477 | /// Load the remote branch with the given name (e.g., "origin/master"). 478 | public func remoteBranch(named name: String) -> Result { 479 | return reference(named: "refs/remotes/" + name).map { $0 as! Branch } 480 | } 481 | 482 | /// Load and return a list of all the `TagReference`s. 483 | public func allTags() -> Result<[TagReference], NSError> { 484 | return references(withPrefix: "refs/tags/") 485 | .map { (refs: [ReferenceType]) in 486 | return refs.map { $0 as! TagReference } 487 | } 488 | } 489 | 490 | /// Load the tag with the given name (e.g., "tag-2"). 491 | public func tag(named name: String) -> Result { 492 | return reference(named: "refs/tags/" + name).map { $0 as! TagReference } 493 | } 494 | 495 | // MARK: - Working Directory 496 | 497 | /// Load the reference pointed at by HEAD. 498 | /// 499 | /// When on a branch, this will return the current `Branch`. 500 | public func HEAD() -> Result { 501 | var pointer: OpaquePointer? = nil 502 | let result = git_repository_head(&pointer, self.pointer) 503 | guard result == GIT_OK.rawValue else { 504 | return Result.failure(NSError(gitError: result, pointOfFailure: "git_repository_head")) 505 | } 506 | let value = referenceWithLibGit2Reference(pointer!) 507 | git_reference_free(pointer) 508 | return Result.success(value) 509 | } 510 | 511 | /// Set HEAD to the given oid (detached). 512 | /// 513 | /// :param: oid The OID to set as HEAD. 514 | /// :returns: Returns a result with void or the error that occurred. 515 | public func setHEAD(_ oid: OID) -> Result<(), NSError> { 516 | var oid = oid.oid 517 | let result = git_repository_set_head_detached(self.pointer, &oid) 518 | guard result == GIT_OK.rawValue else { 519 | return Result.failure(NSError(gitError: result, pointOfFailure: "git_repository_set_head")) 520 | } 521 | return Result.success(()) 522 | } 523 | 524 | /// Set HEAD to the given reference. 525 | /// 526 | /// :param: reference The reference to set as HEAD. 527 | /// :returns: Returns a result with void or the error that occurred. 528 | public func setHEAD(_ reference: ReferenceType) -> Result<(), NSError> { 529 | let result = git_repository_set_head(self.pointer, reference.longName) 530 | guard result == GIT_OK.rawValue else { 531 | return Result.failure(NSError(gitError: result, pointOfFailure: "git_repository_set_head")) 532 | } 533 | return Result.success(()) 534 | } 535 | 536 | /// Check out HEAD. 537 | /// 538 | /// :param: strategy The checkout strategy to use. 539 | /// :param: progress A block that's called with the progress of the checkout. 540 | /// :returns: Returns a result with void or the error that occurred. 541 | public func checkout(strategy: CheckoutStrategy, progress: CheckoutProgressBlock? = nil) -> Result<(), NSError> { 542 | var options = checkoutOptions(strategy: strategy, progress: progress) 543 | 544 | let result = git_checkout_head(self.pointer, &options) 545 | guard result == GIT_OK.rawValue else { 546 | return Result.failure(NSError(gitError: result, pointOfFailure: "git_checkout_head")) 547 | } 548 | 549 | return Result.success(()) 550 | } 551 | 552 | /// Check out the given OID. 553 | /// 554 | /// :param: oid The OID of the commit to check out. 555 | /// :param: strategy The checkout strategy to use. 556 | /// :param: progress A block that's called with the progress of the checkout. 557 | /// :returns: Returns a result with void or the error that occurred. 558 | public func checkout(_ oid: OID, strategy: CheckoutStrategy, 559 | progress: CheckoutProgressBlock? = nil) -> Result<(), NSError> { 560 | return setHEAD(oid).flatMap { self.checkout(strategy: strategy, progress: progress) } 561 | } 562 | 563 | /// Check out the given reference. 564 | /// 565 | /// :param: reference The reference to check out. 566 | /// :param: strategy The checkout strategy to use. 567 | /// :param: progress A block that's called with the progress of the checkout. 568 | /// :returns: Returns a result with void or the error that occurred. 569 | public func checkout(_ reference: ReferenceType, strategy: CheckoutStrategy, 570 | progress: CheckoutProgressBlock? = nil) -> Result<(), NSError> { 571 | return setHEAD(reference).flatMap { self.checkout(strategy: strategy, progress: progress) } 572 | } 573 | 574 | /// Load all commits in the specified branch in topological & time order descending 575 | /// 576 | /// :param: branch The branch to get all commits from 577 | /// :returns: Returns a result with array of branches or the error that occurred 578 | public func commits(in branch: Branch) -> CommitIterator { 579 | let iterator = CommitIterator(repo: self, root: branch.oid.oid) 580 | return iterator 581 | } 582 | 583 | /// Get the index for the repo. The caller is responsible for freeing the index. 584 | func unsafeIndex() -> Result { 585 | var index: OpaquePointer? = nil 586 | let result = git_repository_index(&index, self.pointer) 587 | guard result == GIT_OK.rawValue && index != nil else { 588 | let err = NSError(gitError: result, pointOfFailure: "git_repository_index") 589 | return .failure(err) 590 | } 591 | return .success(index!) 592 | } 593 | 594 | /// Stage the file(s) under the specified path. 595 | public func add(path: String) -> Result<(), NSError> { 596 | var dirPointer = UnsafeMutablePointer(mutating: (path as NSString).utf8String) 597 | var paths = withUnsafeMutablePointer(to: &dirPointer) { 598 | git_strarray(strings: $0, count: 1) 599 | } 600 | return unsafeIndex().flatMap { index in 601 | defer { git_index_free(index) } 602 | let addResult = git_index_add_all(index, &paths, 0, nil, nil) 603 | guard addResult == GIT_OK.rawValue else { 604 | return .failure(NSError(gitError: addResult, pointOfFailure: "git_index_add_all")) 605 | } 606 | // write index to disk 607 | let writeResult = git_index_write(index) 608 | guard writeResult == GIT_OK.rawValue else { 609 | return .failure(NSError(gitError: writeResult, pointOfFailure: "git_index_write")) 610 | } 611 | return .success(()) 612 | } 613 | } 614 | 615 | /// Perform a commit with arbitrary numbers of parent commits. 616 | public func commit( 617 | tree treeOID: OID, 618 | parents: [Commit], 619 | message: String, 620 | signature: Signature 621 | ) -> Result { 622 | // create commit signature 623 | return signature.makeUnsafeSignature().flatMap { signature in 624 | defer { git_signature_free(signature) } 625 | var tree: OpaquePointer? = nil 626 | var treeOIDCopy = treeOID.oid 627 | let lookupResult = git_tree_lookup(&tree, self.pointer, &treeOIDCopy) 628 | guard lookupResult == GIT_OK.rawValue else { 629 | let err = NSError(gitError: lookupResult, pointOfFailure: "git_tree_lookup") 630 | return .failure(err) 631 | } 632 | defer { git_tree_free(tree) } 633 | 634 | var msgBuf = git_buf() 635 | git_message_prettify(&msgBuf, message, 0, /* ascii for # */ 35) 636 | defer { git_buf_free(&msgBuf) } 637 | 638 | // libgit2 expects a C-like array of parent git_commit pointer 639 | var parentGitCommits: [OpaquePointer?] = [] 640 | defer { 641 | for commit in parentGitCommits { 642 | git_commit_free(commit) 643 | } 644 | } 645 | for parentCommit in parents { 646 | var parent: OpaquePointer? = nil 647 | var oid = parentCommit.oid.oid 648 | let lookupResult = git_commit_lookup(&parent, self.pointer, &oid) 649 | guard lookupResult == GIT_OK.rawValue else { 650 | let err = NSError(gitError: lookupResult, pointOfFailure: "git_commit_lookup") 651 | return .failure(err) 652 | } 653 | parentGitCommits.append(parent!) 654 | } 655 | 656 | let parentsContiguous = ContiguousArray(parentGitCommits) 657 | return parentsContiguous.withUnsafeBufferPointer { unsafeBuffer in 658 | var commitOID = git_oid() 659 | let parentsPtr = UnsafeMutablePointer(mutating: unsafeBuffer.baseAddress) 660 | let result = git_commit_create( 661 | &commitOID, 662 | self.pointer, 663 | "HEAD", 664 | signature, 665 | signature, 666 | "UTF-8", 667 | msgBuf.ptr, 668 | tree, 669 | parents.count, 670 | parentsPtr 671 | ) 672 | guard result == GIT_OK.rawValue else { 673 | return .failure(NSError(gitError: result, pointOfFailure: "git_commit_create")) 674 | } 675 | return commit(OID(commitOID)) 676 | } 677 | } 678 | } 679 | 680 | /// Perform a commit of the staged files with the specified message and signature, 681 | /// assuming we are not doing a merge and using the current tip as the parent. 682 | public func commit(message: String, signature: Signature) -> Result { 683 | return unsafeIndex().flatMap { index in 684 | defer { git_index_free(index) } 685 | var treeOID = git_oid() 686 | let treeResult = git_index_write_tree(&treeOID, index) 687 | guard treeResult == GIT_OK.rawValue else { 688 | let err = NSError(gitError: treeResult, pointOfFailure: "git_index_write_tree") 689 | return .failure(err) 690 | } 691 | var parentID = git_oid() 692 | let nameToIDResult = git_reference_name_to_id(&parentID, self.pointer, "HEAD") 693 | guard nameToIDResult == GIT_OK.rawValue else { 694 | return .failure(NSError(gitError: nameToIDResult, pointOfFailure: "git_reference_name_to_id")) 695 | } 696 | return commit(OID(parentID)).flatMap { parentCommit in 697 | commit(tree: OID(treeOID), parents: [parentCommit], message: message, signature: signature) 698 | } 699 | } 700 | } 701 | 702 | // MARK: - Diffs 703 | 704 | public func diff(for commit: Commit) -> Result { 705 | guard !commit.parents.isEmpty else { 706 | // Initial commit in a repository 707 | return self.diff(from: nil, to: commit.oid) 708 | } 709 | 710 | var mergeDiff: OpaquePointer? = nil 711 | defer { git_object_free(mergeDiff) } 712 | for parent in commit.parents { 713 | let error = self.diff(from: parent.oid, to: commit.oid) { 714 | switch $0 { 715 | case .failure(let error): 716 | return error 717 | 718 | case .success(let newDiff): 719 | if mergeDiff == nil { 720 | mergeDiff = newDiff 721 | } else { 722 | let mergeResult = git_diff_merge(mergeDiff, newDiff) 723 | guard mergeResult == GIT_OK.rawValue else { 724 | return NSError(gitError: mergeResult, pointOfFailure: "git_diff_merge") 725 | } 726 | } 727 | return nil 728 | } 729 | } 730 | 731 | if error != nil { 732 | return Result.failure(error!) 733 | } 734 | } 735 | 736 | return .success(Diff(mergeDiff!)) 737 | } 738 | 739 | private func diff(from oldCommitOid: OID?, to newCommitOid: OID?, transform: (Result) -> NSError?) -> NSError? { 740 | assert(oldCommitOid != nil || newCommitOid != nil, "It is an error to pass nil for both the oldOid and newOid") 741 | 742 | var oldTree: OpaquePointer? = nil 743 | defer { git_object_free(oldTree) } 744 | if let oid = oldCommitOid { 745 | switch unsafeTreeForCommitId(oid) { 746 | case .failure(let error): 747 | return transform(.failure(error)) 748 | case .success(let value): 749 | oldTree = value 750 | } 751 | } 752 | 753 | var newTree: OpaquePointer? = nil 754 | defer { git_object_free(newTree) } 755 | if let oid = newCommitOid { 756 | switch unsafeTreeForCommitId(oid) { 757 | case .failure(let error): 758 | return transform(.failure(error)) 759 | case .success(let value): 760 | newTree = value 761 | } 762 | } 763 | 764 | var diff: OpaquePointer? = nil 765 | let diffResult = git_diff_tree_to_tree(&diff, 766 | self.pointer, 767 | oldTree, 768 | newTree, 769 | nil) 770 | 771 | guard diffResult == GIT_OK.rawValue else { 772 | return transform(.failure(NSError(gitError: diffResult, 773 | pointOfFailure: "git_diff_tree_to_tree"))) 774 | } 775 | 776 | return transform(Result.success(diff!)) 777 | } 778 | 779 | /// Memory safe 780 | private func diff(from oldCommitOid: OID?, to newCommitOid: OID?) -> Result { 781 | assert(oldCommitOid != nil || newCommitOid != nil, "It is an error to pass nil for both the oldOid and newOid") 782 | 783 | var oldTree: Tree? = nil 784 | if let oldCommitOid = oldCommitOid { 785 | switch safeTreeForCommitId(oldCommitOid) { 786 | case .failure(let error): 787 | return .failure(error) 788 | case .success(let value): 789 | oldTree = value 790 | } 791 | } 792 | 793 | var newTree: Tree? = nil 794 | if let newCommitOid = newCommitOid { 795 | switch safeTreeForCommitId(newCommitOid) { 796 | case .failure(let error): 797 | return .failure(error) 798 | case .success(let value): 799 | newTree = value 800 | } 801 | } 802 | 803 | if oldTree != nil && newTree != nil { 804 | return withGitObjects([oldTree!.oid, newTree!.oid], type: GIT_OBJECT_TREE) { objects in 805 | var diff: OpaquePointer? = nil 806 | let diffResult = git_diff_tree_to_tree(&diff, 807 | self.pointer, 808 | objects[0], 809 | objects[1], 810 | nil) 811 | return processTreeToTreeDiff(diffResult, diff: diff) 812 | } 813 | } else if let tree = oldTree { 814 | return withGitObject(tree.oid, type: GIT_OBJECT_TREE, transform: { tree in 815 | var diff: OpaquePointer? = nil 816 | let diffResult = git_diff_tree_to_tree(&diff, 817 | self.pointer, 818 | tree, 819 | nil, 820 | nil) 821 | return processTreeToTreeDiff(diffResult, diff: diff) 822 | }) 823 | } else if let tree = newTree { 824 | return withGitObject(tree.oid, type: GIT_OBJECT_TREE, transform: { tree in 825 | var diff: OpaquePointer? = nil 826 | let diffResult = git_diff_tree_to_tree(&diff, 827 | self.pointer, 828 | nil, 829 | tree, 830 | nil) 831 | return processTreeToTreeDiff(diffResult, diff: diff) 832 | }) 833 | } 834 | 835 | return .failure(NSError(gitError: -1, pointOfFailure: "diff(from: to:)")) 836 | } 837 | 838 | private func processTreeToTreeDiff(_ diffResult: Int32, diff: OpaquePointer?) -> Result { 839 | guard diffResult == GIT_OK.rawValue else { 840 | return .failure(NSError(gitError: diffResult, 841 | pointOfFailure: "git_diff_tree_to_tree")) 842 | } 843 | 844 | let diffObj = Diff(diff!) 845 | git_diff_free(diff) 846 | return .success(diffObj) 847 | } 848 | 849 | private func processDiffDeltas(_ diffResult: OpaquePointer) -> Result<[Diff.Delta], NSError> { 850 | var returnDict = [Diff.Delta]() 851 | 852 | let count = git_diff_num_deltas(diffResult) 853 | 854 | for i in 0...success(returnDict) 862 | return result 863 | } 864 | 865 | private func safeTreeForCommitId(_ oid: OID) -> Result { 866 | return withGitObject(oid, type: GIT_OBJECT_COMMIT) { commit in 867 | let treeId = git_commit_tree_id(commit) 868 | return tree(OID(treeId!.pointee)) 869 | } 870 | } 871 | 872 | /// Caller responsible to free returned tree with git_object_free 873 | private func unsafeTreeForCommitId(_ oid: OID) -> Result { 874 | var commit: OpaquePointer? = nil 875 | var oid = oid.oid 876 | let commitResult = git_object_lookup(&commit, self.pointer, &oid, GIT_OBJECT_COMMIT) 877 | guard commitResult == GIT_OK.rawValue else { 878 | return .failure(NSError(gitError: commitResult, pointOfFailure: "git_object_lookup")) 879 | } 880 | 881 | var tree: OpaquePointer? = nil 882 | let treeId = git_commit_tree_id(commit) 883 | let treeResult = git_object_lookup(&tree, self.pointer, treeId, GIT_OBJECT_TREE) 884 | 885 | git_object_free(commit) 886 | 887 | guard treeResult == GIT_OK.rawValue else { 888 | return .failure(NSError(gitError: treeResult, pointOfFailure: "git_object_lookup")) 889 | } 890 | 891 | return Result.success(tree!) 892 | } 893 | 894 | // MARK: - Status 895 | 896 | public func status() -> Result<[StatusEntry], NSError> { 897 | var returnArray = [StatusEntry]() 898 | 899 | // Do this because GIT_STATUS_OPTIONS_INIT is unavailable in swift 900 | let pointer = UnsafeMutablePointer.allocate(capacity: 1) 901 | let optionsResult = git_status_init_options(pointer, UInt32(GIT_STATUS_OPTIONS_VERSION)) 902 | guard optionsResult == GIT_OK.rawValue else { 903 | return .failure(NSError(gitError: optionsResult, pointOfFailure: "git_status_init_options")) 904 | } 905 | var options = pointer.move() 906 | pointer.deallocate() 907 | 908 | var unsafeStatus: OpaquePointer? = nil 909 | defer { git_status_list_free(unsafeStatus) } 910 | let statusResult = git_status_list_new(&unsafeStatus, self.pointer, &options) 911 | guard statusResult == GIT_OK.rawValue, let unwrapStatusResult = unsafeStatus else { 912 | return .failure(NSError(gitError: statusResult, pointOfFailure: "git_status_list_new")) 913 | } 914 | 915 | let count = git_status_list_entrycount(unwrapStatusResult) 916 | 917 | for i in 0.. Result { 936 | var pointer: OpaquePointer? 937 | 938 | let result = url.withUnsafeFileSystemRepresentation { 939 | git_repository_open_ext(&pointer, $0, GIT_REPOSITORY_OPEN_NO_SEARCH.rawValue, nil) 940 | } 941 | 942 | switch result { 943 | case GIT_ENOTFOUND.rawValue: 944 | return .success(false) 945 | case GIT_OK.rawValue: 946 | return .success(true) 947 | default: 948 | return .failure(NSError(gitError: result, pointOfFailure: "git_repository_open_ext")) 949 | } 950 | } 951 | } 952 | 953 | private extension Array { 954 | func aggregateResult() -> Result<[Value], Error> where Element == Result { 955 | var values: [Value] = [] 956 | for result in self { 957 | switch result { 958 | case .success(let value): 959 | values.append(value) 960 | case .failure(let error): 961 | return .failure(error) 962 | } 963 | } 964 | return .success(values) 965 | } 966 | } 967 | -------------------------------------------------------------------------------- /Tests/GitTests/RepositorySpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepositorySpec.swift 3 | // RepositorySpec 4 | // 5 | // Created by Matt Diephouse on 11/7/14. 6 | // Copyright (c) 2014 GitHub, Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Git 11 | import Nimble 12 | import Quick 13 | 14 | // swiftlint:disable cyclomatic_complexity 15 | 16 | class RepositorySpec: FixturesSpec { 17 | override func spec() { 18 | describe("Repository.Type.at(_:)") { 19 | it("should work if the repo exists") { 20 | let repo = Fixtures.simpleRepository 21 | expect(repo.directoryURL).notTo(beNil()) 22 | } 23 | 24 | it("should fail if the repo doesn't exist") { 25 | let url = URL(fileURLWithPath: "blah") 26 | let result = Repository.at(url) 27 | expect(result.error?.domain) == libGit2ErrorDomain 28 | expect(result.error?.localizedDescription).to(match("failed to resolve path")) 29 | } 30 | } 31 | 32 | describe("Repository.Type.isValid(url:)") { 33 | it("should return true if the repo exists") { 34 | guard let repositoryURL = Fixtures.simpleRepository.directoryURL else { 35 | fail("Fixture setup broken: Repository does not exist"); return 36 | } 37 | 38 | let result = Repository.isValid(url: repositoryURL) 39 | 40 | expect(result.error).to(beNil()) 41 | 42 | if case .success(let isValid) = result { 43 | expect(isValid).to(beTruthy()) 44 | } 45 | } 46 | 47 | it("should return false if the directory does not contain a repo") { 48 | let tmpURL = URL(fileURLWithPath: "/dev/null") 49 | let result = Repository.isValid(url: tmpURL) 50 | 51 | expect(result.error).to(beNil()) 52 | 53 | if case .success(let isValid) = result { 54 | expect(isValid).to(beFalsy()) 55 | } 56 | } 57 | 58 | it("should return error if .git is not readable") { 59 | let localURL = self.temporaryURL(forPurpose: "git-isValid-unreadable").appendingPathComponent(".git") 60 | let nonReadablePermissions: [FileAttributeKey: Any] = [.posixPermissions: 0o077] 61 | 62 | do { 63 | try FileManager.default.createDirectory( 64 | at: localURL, 65 | withIntermediateDirectories: true, 66 | attributes: nonReadablePermissions) 67 | let result = Repository.isValid(url: localURL) 68 | 69 | expect(result.value).to(beNil()) 70 | expect(result.error).notTo(beNil()) 71 | } catch { 72 | print("Warning: Could not create non-readable .git, skipping the test...") 73 | } 74 | } 75 | } 76 | 77 | describe("Repository.Type.create(at:)") { 78 | it("should create a new repo at the specified location") { 79 | let localURL = self.temporaryURL(forPurpose: "local-create") 80 | let result = Repository.create(at: localURL) 81 | 82 | expect(result.error).to(beNil()) 83 | 84 | if case .success(let clonedRepo) = result { 85 | expect(clonedRepo.directoryURL).notTo(beNil()) 86 | } 87 | } 88 | } 89 | 90 | describe("Repository.Type.clone(from:to:)") { 91 | it("should handle local clones") { 92 | let remoteRepo = Fixtures.simpleRepository 93 | let localURL = self.temporaryURL(forPurpose: "local-clone") 94 | let result = Repository.clone(from: remoteRepo.directoryURL!, to: localURL, localClone: true) 95 | 96 | expect(result.error).to(beNil()) 97 | 98 | if case .success(let clonedRepo) = result { 99 | expect(clonedRepo.directoryURL).notTo(beNil()) 100 | } 101 | } 102 | 103 | it("should handle bare clones") { 104 | let remoteRepo = Fixtures.simpleRepository 105 | let localURL = self.temporaryURL(forPurpose: "bare-clone") 106 | let result = Repository.clone(from: remoteRepo.directoryURL!, to: localURL, localClone: true, bare: true) 107 | 108 | expect(result.error).to(beNil()) 109 | 110 | if case .success(let clonedRepo) = result { 111 | expect(clonedRepo.directoryURL).to(beNil()) 112 | } 113 | } 114 | 115 | it("should have set a valid remote url") { 116 | let remoteRepo = Fixtures.simpleRepository 117 | let localURL = self.temporaryURL(forPurpose: "valid-remote-clone") 118 | let cloneResult = Repository.clone(from: remoteRepo.directoryURL!, to: localURL, localClone: true) 119 | 120 | expect(cloneResult.error).to(beNil()) 121 | 122 | if case .success(let clonedRepo) = cloneResult { 123 | let remoteResult = clonedRepo.remote(named: "origin") 124 | expect(remoteResult.error).to(beNil()) 125 | 126 | if case .success(let remote) = remoteResult { 127 | expect(remote.URL).to(equal(remoteRepo.directoryURL?.path)) 128 | } 129 | } 130 | } 131 | 132 | it("should be able to clone a remote repository") { 133 | let remoteRepoURL = URL(string: "https://github.com/libgit2/libgit2.github.com.git") 134 | let localURL = self.temporaryURL(forPurpose: "public-remote-clone") 135 | let cloneResult = Repository.clone(from: remoteRepoURL!, to: localURL) 136 | 137 | expect(cloneResult.error).to(beNil()) 138 | 139 | if case .success(let clonedRepo) = cloneResult { 140 | let remoteResult = clonedRepo.remote(named: "origin") 141 | expect(remoteResult.error).to(beNil()) 142 | 143 | if case .success(let remote) = remoteResult { 144 | expect(remote.URL).to(equal(remoteRepoURL?.absoluteString)) 145 | } 146 | } 147 | } 148 | 149 | let env = ProcessInfo.processInfo.environment 150 | 151 | if let privateRepo = env["SG2TestPrivateRepo"], 152 | let gitUsername = env["SG2TestUsername"], 153 | let publicKey = env["SG2TestPublicKey"], 154 | let privateKey = env["SG2TestPrivateKey"], 155 | let passphrase = env["SG2TestPassphrase"] { 156 | 157 | it("should be able to clone a remote repository requiring credentials") { 158 | let remoteRepoURL = URL(string: privateRepo) 159 | let localURL = self.temporaryURL(forPurpose: "private-remote-clone") 160 | let credentials = Credentials.sshMemory(username: gitUsername, 161 | publicKey: publicKey, 162 | privateKey: privateKey, 163 | passphrase: passphrase) 164 | 165 | let cloneResult = Repository.clone(from: remoteRepoURL!, to: localURL, credentials: credentials) 166 | 167 | expect(cloneResult.error).to(beNil()) 168 | 169 | if case .success(let clonedRepo) = cloneResult { 170 | let remoteResult = clonedRepo.remote(named: "origin") 171 | expect(remoteResult.error).to(beNil()) 172 | 173 | if case .success(let remote) = remoteResult { 174 | expect(remote.URL).to(equal(remoteRepoURL?.absoluteString)) 175 | } 176 | } 177 | } 178 | } 179 | } 180 | 181 | describe("Repository.blob(_:)") { 182 | it("should return the commit if it exists") { 183 | let repo = Fixtures.simpleRepository 184 | let oid = OID(string: "41078396f5187daed5f673e4a13b185bbad71fba")! 185 | 186 | let result = repo.blob(oid) 187 | expect(result.map { $0.oid }.value) == oid 188 | } 189 | 190 | it("should error if the blob doesn't exist") { 191 | let repo = Fixtures.simpleRepository 192 | let oid = OID(string: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")! 193 | 194 | let result = repo.blob(oid) 195 | expect(result.error?.domain) == libGit2ErrorDomain 196 | } 197 | 198 | it("should error if the oid doesn't point to a blob") { 199 | let repo = Fixtures.simpleRepository 200 | // This is a tree in the repository 201 | let oid = OID(string: "f93e3a1a1525fb5b91020da86e44810c87a2d7bc")! 202 | 203 | let result = repo.blob(oid) 204 | expect(result.error).notTo(beNil()) 205 | } 206 | } 207 | 208 | describe("Repository.commit(_:)") { 209 | it("should return the commit if it exists") { 210 | let repo = Fixtures.simpleRepository 211 | let oid = OID(string: "dc220a3f0c22920dab86d4a8d3a3cb7e69d6205a")! 212 | 213 | let result = repo.commit(oid) 214 | expect(result.map { $0.oid }.value) == oid 215 | } 216 | 217 | it("should error if the commit doesn't exist") { 218 | let repo = Fixtures.simpleRepository 219 | let oid = OID(string: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")! 220 | 221 | let result = repo.commit(oid) 222 | expect(result.error?.domain) == libGit2ErrorDomain 223 | } 224 | 225 | it("should error if the oid doesn't point to a commit") { 226 | let repo = Fixtures.simpleRepository 227 | // This is a tree in the repository 228 | let oid = OID(string: "f93e3a1a1525fb5b91020da86e44810c87a2d7bc")! 229 | 230 | let result = repo.commit(oid) 231 | expect(result.error?.domain) == libGit2ErrorDomain 232 | } 233 | } 234 | 235 | describe("Repository.tag(_:)") { 236 | it("should return the tag if it exists") { 237 | let repo = Fixtures.simpleRepository 238 | let oid = OID(string: "57943b8ee00348180ceeedc960451562750f6d33")! 239 | 240 | let result = repo.tag(oid) 241 | expect(result.map { $0.oid }.value) == oid 242 | } 243 | 244 | it("should error if the tag doesn't exist") { 245 | let repo = Fixtures.simpleRepository 246 | let oid = OID(string: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")! 247 | 248 | let result = repo.tag(oid) 249 | expect(result.error?.domain) == libGit2ErrorDomain 250 | } 251 | 252 | it("should error if the oid doesn't point to a tag") { 253 | let repo = Fixtures.simpleRepository 254 | // This is a commit in the repository 255 | let oid = OID(string: "dc220a3f0c22920dab86d4a8d3a3cb7e69d6205a")! 256 | 257 | let result = repo.tag(oid) 258 | expect(result.error?.domain) == libGit2ErrorDomain 259 | } 260 | } 261 | 262 | describe("Repository.tree(_:)") { 263 | it("should return the tree if it exists") { 264 | let repo = Fixtures.simpleRepository 265 | let oid = OID(string: "f93e3a1a1525fb5b91020da86e44810c87a2d7bc")! 266 | 267 | let result = repo.tree(oid) 268 | expect(result.map { $0.oid }.value) == oid 269 | } 270 | 271 | it("should error if the tree doesn't exist") { 272 | let repo = Fixtures.simpleRepository 273 | let oid = OID(string: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")! 274 | 275 | let result = repo.tree(oid) 276 | expect(result.error?.domain) == libGit2ErrorDomain 277 | } 278 | 279 | it("should error if the oid doesn't point to a tree") { 280 | let repo = Fixtures.simpleRepository 281 | // This is a commit in the repository 282 | let oid = OID(string: "dc220a3f0c22920dab86d4a8d3a3cb7e69d6205a")! 283 | 284 | let result = repo.tree(oid) 285 | expect(result.error?.domain) == libGit2ErrorDomain 286 | } 287 | } 288 | 289 | describe("Repository.object(_:)") { 290 | it("should work with a blob") { 291 | let repo = Fixtures.simpleRepository 292 | let oid = OID(string: "41078396f5187daed5f673e4a13b185bbad71fba")! 293 | let blob = repo.blob(oid).value 294 | let result = repo.object(oid) 295 | expect(result.map { $0 as! Blob }.value) == blob 296 | } 297 | 298 | it("should work with a commit") { 299 | let repo = Fixtures.simpleRepository 300 | let oid = OID(string: "dc220a3f0c22920dab86d4a8d3a3cb7e69d6205a")! 301 | let commit = repo.commit(oid).value 302 | let result = repo.object(oid) 303 | expect(result.map { $0 as! Commit }.value) == commit 304 | } 305 | 306 | it("should work with a tag") { 307 | let repo = Fixtures.simpleRepository 308 | let oid = OID(string: "57943b8ee00348180ceeedc960451562750f6d33")! 309 | let tag = repo.tag(oid).value 310 | let result = repo.object(oid) 311 | expect(result.map { $0 as! Tag }.value) == tag 312 | } 313 | 314 | it("should work with a tree") { 315 | let repo = Fixtures.simpleRepository 316 | let oid = OID(string: "f93e3a1a1525fb5b91020da86e44810c87a2d7bc")! 317 | let tree = repo.tree(oid).value 318 | let result = repo.object(oid) 319 | expect(result.map { $0 as! Tree }.value) == tree 320 | } 321 | 322 | it("should error if there's no object with that oid") { 323 | let repo = Fixtures.simpleRepository 324 | let oid = OID(string: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")! 325 | let result = repo.object(oid) 326 | expect(result.error?.domain) == libGit2ErrorDomain 327 | } 328 | } 329 | 330 | describe("Repository.object(from: PointerTo)") { 331 | it("should work with commits") { 332 | let repo = Fixtures.simpleRepository 333 | let oid = OID(string: "dc220a3f0c22920dab86d4a8d3a3cb7e69d6205a")! 334 | 335 | let pointer = PointerTo(oid) 336 | let commit = repo.commit(oid).value! 337 | expect(repo.object(from: pointer).value) == commit 338 | } 339 | 340 | it("should work with trees") { 341 | let repo = Fixtures.simpleRepository 342 | let oid = OID(string: "f93e3a1a1525fb5b91020da86e44810c87a2d7bc")! 343 | 344 | let pointer = PointerTo(oid) 345 | let tree = repo.tree(oid).value! 346 | expect(repo.object(from: pointer).value) == tree 347 | } 348 | 349 | it("should work with blobs") { 350 | let repo = Fixtures.simpleRepository 351 | let oid = OID(string: "41078396f5187daed5f673e4a13b185bbad71fba")! 352 | 353 | let pointer = PointerTo(oid) 354 | let blob = repo.blob(oid).value! 355 | expect(repo.object(from: pointer).value) == blob 356 | } 357 | 358 | it("should work with tags") { 359 | let repo = Fixtures.simpleRepository 360 | let oid = OID(string: "57943b8ee00348180ceeedc960451562750f6d33")! 361 | 362 | let pointer = PointerTo(oid) 363 | let tag = repo.tag(oid).value! 364 | expect(repo.object(from: pointer).value) == tag 365 | } 366 | } 367 | 368 | describe("Repository.object(from: Pointer)") { 369 | it("should work with commits") { 370 | let repo = Fixtures.simpleRepository 371 | let oid = OID(string: "dc220a3f0c22920dab86d4a8d3a3cb7e69d6205a")! 372 | 373 | let pointer = Pointer.commit(oid) 374 | let commit = repo.commit(oid).value! 375 | let result = repo.object(from: pointer).map { $0 as! Commit } 376 | expect(result.value) == commit 377 | } 378 | 379 | it("should work with trees") { 380 | let repo = Fixtures.simpleRepository 381 | let oid = OID(string: "f93e3a1a1525fb5b91020da86e44810c87a2d7bc")! 382 | 383 | let pointer = Pointer.tree(oid) 384 | let tree = repo.tree(oid).value! 385 | let result = repo.object(from: pointer).map { $0 as! Tree } 386 | expect(result.value) == tree 387 | } 388 | 389 | it("should work with blobs") { 390 | let repo = Fixtures.simpleRepository 391 | let oid = OID(string: "41078396f5187daed5f673e4a13b185bbad71fba")! 392 | 393 | let pointer = Pointer.blob(oid) 394 | let blob = repo.blob(oid).value! 395 | let result = repo.object(from: pointer).map { $0 as! Blob } 396 | expect(result.value) == blob 397 | } 398 | 399 | it("should work with tags") { 400 | let repo = Fixtures.simpleRepository 401 | let oid = OID(string: "57943b8ee00348180ceeedc960451562750f6d33")! 402 | 403 | let pointer = Pointer.tag(oid) 404 | let tag = repo.tag(oid).value! 405 | let result = repo.object(from: pointer).map { $0 as! Tag } 406 | expect(result.value) == tag 407 | } 408 | } 409 | 410 | describe("Repository.allRemotes()") { 411 | it("should return an empty list if there are no remotes") { 412 | let repo = Fixtures.simpleRepository 413 | let result = repo.allRemotes() 414 | expect(result.value) == [] 415 | } 416 | 417 | it("should return all the remotes") { 418 | let repo = Fixtures.mantleRepository 419 | let remotes = repo.allRemotes() 420 | let names = remotes.map { $0.map { $0.name } } 421 | expect(remotes.map { $0.count }.value) == 2 422 | expect(names.value).to(contain("origin", "upstream")) 423 | } 424 | } 425 | 426 | describe("Repository.remote(named:)") { 427 | it("should return the remote if it exists") { 428 | let repo = Fixtures.mantleRepository 429 | let result = repo.remote(named: "upstream") 430 | expect(result.map { $0.name }.value) == "upstream" 431 | } 432 | 433 | it("should error if the remote doesn't exist") { 434 | let repo = Fixtures.simpleRepository 435 | let result = repo.remote(named: "nonexistent") 436 | expect(result.error?.domain) == libGit2ErrorDomain 437 | } 438 | } 439 | 440 | describe("Repository.reference(named:)") { 441 | it("should return a local branch if it exists") { 442 | let name = "refs/heads/master" 443 | let result = Fixtures.simpleRepository.reference(named: name) 444 | expect(result.map { $0.longName }.value) == name 445 | expect(result.value as? Branch).notTo(beNil()) 446 | } 447 | 448 | it("should return a remote branch if it exists") { 449 | let name = "refs/remotes/upstream/master" 450 | let result = Fixtures.mantleRepository.reference(named: name) 451 | expect(result.map { $0.longName }.value) == name 452 | expect(result.value as? Branch).notTo(beNil()) 453 | } 454 | 455 | it("should return a tag if it exists") { 456 | let name = "refs/tags/tag-2" 457 | let result = Fixtures.simpleRepository.reference(named: name) 458 | expect(result.value?.longName).to(equal(name)) 459 | expect(result.value as? TagReference).notTo(beNil()) 460 | } 461 | 462 | it("should return the reference if it exists") { 463 | let name = "refs/other-ref" 464 | let result = Fixtures.simpleRepository.reference(named: name) 465 | expect(result.value?.longName).to(equal(name)) 466 | } 467 | 468 | it("should error if the reference doesn't exist") { 469 | let result = Fixtures.simpleRepository.reference(named: "refs/heads/nonexistent") 470 | expect(result.error?.domain) == libGit2ErrorDomain 471 | } 472 | } 473 | 474 | describe("Repository.localBranches()") { 475 | it("should return all the local branches") { 476 | let repo = Fixtures.simpleRepository 477 | let expected = [ 478 | repo.localBranch(named: "another-branch").value!, 479 | repo.localBranch(named: "master").value!, 480 | repo.localBranch(named: "yet-another-branch").value!, 481 | ] 482 | expect(repo.localBranches().value).to(equal(expected)) 483 | } 484 | } 485 | 486 | describe("Repository.remoteBranches()") { 487 | it("should return all the remote branches") { 488 | let repo = Fixtures.mantleRepository 489 | let expectedNames = [ 490 | "origin/2.0-development", 491 | "origin/HEAD", 492 | "origin/bump-config", 493 | "origin/bump-xcconfigs", 494 | "origin/github-reversible-transformer", 495 | "origin/master", 496 | "origin/mtlmanagedobject", 497 | "origin/reversible-transformer", 498 | "origin/subclassing-notes", 499 | "upstream/2.0-development", 500 | "upstream/bump-config", 501 | "upstream/bump-xcconfigs", 502 | "upstream/github-reversible-transformer", 503 | "upstream/master", 504 | "upstream/mtlmanagedobject", 505 | "upstream/reversible-transformer", 506 | "upstream/subclassing-notes", 507 | ] 508 | let expected = expectedNames.map { repo.remoteBranch(named: $0).value! } 509 | let actual = repo.remoteBranches().value!.sorted { 510 | return $0.longName.lexicographicallyPrecedes($1.longName) 511 | } 512 | expect(actual).to(equal(expected)) 513 | expect(actual.map { $0.name }).to(equal(expectedNames)) 514 | } 515 | } 516 | 517 | describe("Repository.localBranch(named:)") { 518 | it("should return the branch if it exists") { 519 | let result = Fixtures.simpleRepository.localBranch(named: "master") 520 | expect(result.value?.longName).to(equal("refs/heads/master")) 521 | } 522 | 523 | it("should error if the branch doesn't exists") { 524 | let result = Fixtures.simpleRepository.localBranch(named: "nonexistent") 525 | expect(result.error?.domain) == libGit2ErrorDomain 526 | } 527 | } 528 | 529 | describe("Repository.remoteBranch(named:)") { 530 | it("should return the branch if it exists") { 531 | let result = Fixtures.mantleRepository.remoteBranch(named: "upstream/master") 532 | expect(result.value?.longName).to(equal("refs/remotes/upstream/master")) 533 | } 534 | 535 | it("should error if the branch doesn't exists") { 536 | let result = Fixtures.simpleRepository.remoteBranch(named: "origin/nonexistent") 537 | expect(result.error?.domain) == libGit2ErrorDomain 538 | } 539 | } 540 | 541 | describe("Repository.fetch(_:)") { 542 | it("should fetch the data") { 543 | let repo = Fixtures.mantleRepository 544 | let remote = repo.remote(named: "origin").value! 545 | expect(repo.fetch(remote).value).toNot(beNil()) 546 | } 547 | } 548 | 549 | describe("Repository.allTags()") { 550 | it("should return all the tags") { 551 | let repo = Fixtures.simpleRepository 552 | let expected = [ 553 | repo.tag(named: "tag-1").value!, 554 | repo.tag(named: "tag-2").value!, 555 | ] 556 | expect(repo.allTags().value).to(equal(expected)) 557 | } 558 | } 559 | 560 | describe("Repository.tag(named:)") { 561 | it("should return the tag if it exists") { 562 | let result = Fixtures.simpleRepository.tag(named: "tag-2") 563 | expect(result.value?.longName).to(equal("refs/tags/tag-2")) 564 | } 565 | 566 | it("should error if the branch doesn't exists") { 567 | let result = Fixtures.simpleRepository.tag(named: "nonexistent") 568 | expect(result.error?.domain) == libGit2ErrorDomain 569 | } 570 | } 571 | 572 | describe("Repository.HEAD()") { 573 | it("should work when on a branch") { 574 | let result = Fixtures.simpleRepository.HEAD() 575 | expect(result.value?.longName).to(equal("refs/heads/master")) 576 | expect(result.value?.shortName).to(equal("master")) 577 | expect(result.value as? Branch).notTo(beNil()) 578 | } 579 | 580 | it("should work when on a detached HEAD") { 581 | let result = Fixtures.detachedHeadRepository.HEAD() 582 | expect(result.value?.longName).to(equal("HEAD")) 583 | expect(result.value?.shortName).to(beNil()) 584 | expect(result.value?.oid).to(equal(OID(string: "315b3f344221db91ddc54b269f3c9af422da0f2e")!)) 585 | expect(result.value as? Reference).notTo(beNil()) 586 | } 587 | } 588 | 589 | describe("Repository.setHEAD(OID)") { 590 | it("should set HEAD to the OID") { 591 | let repo = Fixtures.simpleRepository 592 | let oid = OID(string: "315b3f344221db91ddc54b269f3c9af422da0f2e")! 593 | expect(repo.HEAD().value?.shortName).to(equal("master")) 594 | 595 | expect(repo.setHEAD(oid).error).to(beNil()) 596 | let HEAD = repo.HEAD().value 597 | expect(HEAD?.longName).to(equal("HEAD")) 598 | expect(HEAD?.oid).to(equal(oid)) 599 | 600 | expect(repo.setHEAD(repo.localBranch(named: "master").value!).error).to(beNil()) 601 | expect(repo.HEAD().value?.shortName).to(equal("master")) 602 | } 603 | } 604 | 605 | describe("Repository.setHEAD(ReferenceType)") { 606 | it("should set HEAD to a branch") { 607 | let repo = Fixtures.detachedHeadRepository 608 | let oid = repo.HEAD().value!.oid 609 | expect(repo.HEAD().value?.longName).to(equal("HEAD")) 610 | 611 | let branch = repo.localBranch(named: "another-branch").value! 612 | expect(repo.setHEAD(branch).error).to(beNil()) 613 | expect(repo.HEAD().value?.shortName).to(equal(branch.name)) 614 | 615 | expect(repo.setHEAD(oid).error).to(beNil()) 616 | expect(repo.HEAD().value?.longName).to(equal("HEAD")) 617 | } 618 | } 619 | 620 | describe("Repository.checkout()") { 621 | // We're not really equipped to test this yet. :( 622 | } 623 | 624 | describe("Repository.checkout(OID)") { 625 | it("should set HEAD") { 626 | let repo = Fixtures.simpleRepository 627 | let oid = OID(string: "315b3f344221db91ddc54b269f3c9af422da0f2e")! 628 | expect(repo.HEAD().value?.shortName).to(equal("master")) 629 | 630 | expect(repo.checkout(oid, strategy: CheckoutStrategy.None).error).to(beNil()) 631 | let HEAD = repo.HEAD().value 632 | expect(HEAD?.longName).to(equal("HEAD")) 633 | expect(HEAD?.oid).to(equal(oid)) 634 | 635 | expect(repo.checkout(repo.localBranch(named: "master").value!, strategy: CheckoutStrategy.None).error).to(beNil()) 636 | expect(repo.HEAD().value?.shortName).to(equal("master")) 637 | } 638 | 639 | it("should call block on progress") { 640 | let repo = Fixtures.simpleRepository 641 | let oid = OID(string: "315b3f344221db91ddc54b269f3c9af422da0f2e")! 642 | expect(repo.HEAD().value?.shortName).to(equal("master")) 643 | 644 | expect(repo.checkout(oid, strategy: .None, progress: { (_, completedSteps, totalSteps) -> Void in 645 | expect(completedSteps).to(beLessThanOrEqualTo(totalSteps)) 646 | }).error).to(beNil()) 647 | 648 | let HEAD = repo.HEAD().value 649 | expect(HEAD?.longName).to(equal("HEAD")) 650 | expect(HEAD?.oid).to(equal(oid)) 651 | } 652 | } 653 | 654 | describe("Repository.checkout(ReferenceType)") { 655 | it("should set HEAD") { 656 | let repo = Fixtures.detachedHeadRepository 657 | let oid = repo.HEAD().value!.oid 658 | expect(repo.HEAD().value?.longName).to(equal("HEAD")) 659 | 660 | let branch = repo.localBranch(named: "another-branch").value! 661 | expect(repo.checkout(branch, strategy: CheckoutStrategy.None).error).to(beNil()) 662 | expect(repo.HEAD().value?.shortName).to(equal(branch.name)) 663 | 664 | expect(repo.checkout(oid, strategy: CheckoutStrategy.None).error).to(beNil()) 665 | expect(repo.HEAD().value?.longName).to(equal("HEAD")) 666 | } 667 | } 668 | 669 | describe("Repository.allCommits(in:)") { 670 | it("should return all (9) commits") { 671 | let repo = Fixtures.simpleRepository 672 | let branches = repo.localBranches().value! 673 | let expectedCount = 9 674 | let expectedMessages: [String] = [ 675 | "List branches in README\n", 676 | "Create a README\n", 677 | "Merge branch 'alphabetize'\n", 678 | "Alphabetize branches\n", 679 | "List new branches\n", 680 | "List branches in README\n", 681 | "Create a README\n", 682 | "List branches in README\n", 683 | "Create a README\n", 684 | ] 685 | var commitMessages: [String] = [] 686 | for branch in branches { 687 | for commit in repo.commits(in: branch) { 688 | commitMessages.append(commit.value!.message) 689 | } 690 | } 691 | expect(commitMessages.count).to(equal(expectedCount)) 692 | expect(commitMessages).to(equal(expectedMessages)) 693 | } 694 | } 695 | 696 | describe("Repository.add") { 697 | it("Should add the modification under a path") { 698 | let repo = Fixtures.simpleRepository 699 | let branch = repo.localBranch(named: "master").value! 700 | expect(repo.checkout(branch, strategy: CheckoutStrategy.None).error).to(beNil()) 701 | 702 | // make a change to README 703 | let readmeURL = repo.directoryURL!.appendingPathComponent("README.md") 704 | let readmeData = try! Data(contentsOf: readmeURL) 705 | defer { try! readmeData.write(to: readmeURL) } 706 | 707 | try! "different".data(using: .utf8)?.write(to: readmeURL) 708 | 709 | let status = repo.status() 710 | expect(status.value?.count).to(equal(1)) 711 | expect(status.value!.first!.status).to(equal(.workTreeModified)) 712 | 713 | expect(repo.add(path: "README.md").error).to(beNil()) 714 | 715 | let newStatus = repo.status() 716 | expect(newStatus.value?.count).to(equal(1)) 717 | expect(newStatus.value!.first!.status).to(equal(.indexModified)) 718 | } 719 | 720 | it("Should add an untracked file under a path") { 721 | let repo = Fixtures.simpleRepository 722 | let branch = repo.localBranch(named: "master").value! 723 | expect(repo.checkout(branch, strategy: CheckoutStrategy.None).error).to(beNil()) 724 | 725 | // make a change to README 726 | let untrackedURL = repo.directoryURL!.appendingPathComponent("untracked") 727 | try! "different".data(using: .utf8)?.write(to: untrackedURL) 728 | defer { try! FileManager.default.removeItem(at: untrackedURL) } 729 | 730 | expect(repo.add(path: ".").error).to(beNil()) 731 | 732 | let newStatus = repo.status() 733 | expect(newStatus.value?.count).to(equal(1)) 734 | expect(newStatus.value!.first!.status).to(equal(.indexNew)) 735 | } 736 | } 737 | 738 | describe("Repository.commit") { 739 | it("Should perform a simple commit with specified signature") { 740 | let repo = Fixtures.simpleRepository 741 | let branch = repo.localBranch(named: "master").value! 742 | expect(repo.checkout(branch, strategy: CheckoutStrategy.None).error).to(beNil()) 743 | 744 | // make a change to README 745 | let untrackedURL = repo.directoryURL!.appendingPathComponent("untrackedtest") 746 | try! "different".data(using: .utf8)?.write(to: untrackedURL) 747 | 748 | expect(repo.add(path: ".").error).to(beNil()) 749 | 750 | let signature = Signature( 751 | name: "swiftgit2", 752 | email: "foobar@example.com", 753 | time: Date(timeIntervalSince1970: 1525200858), 754 | timeZone: TimeZone(secondsFromGMT: 3600)! 755 | ) 756 | let message = "Test Commit" 757 | expect(repo.commit(message: message, signature: signature).error).to(beNil()) 758 | let updatedBranch = repo.localBranch(named: "master").value! 759 | expect(repo.commits(in: updatedBranch).next()?.value?.author).to(equal(signature)) 760 | expect(repo.commits(in: updatedBranch).next()?.value?.committer).to(equal(signature)) 761 | expect(repo.commits(in: updatedBranch).next()?.value?.message).to(equal("\(message)\n")) 762 | expect(repo.commits(in: updatedBranch).next()?.value?.oid.description) 763 | .to(equal("7d6b2d7492f29aee48022387f96dbfe996d435fe")) 764 | 765 | // should be clean now 766 | let newStatus = repo.status() 767 | expect(newStatus.value?.count).to(equal(0)) 768 | } 769 | } 770 | 771 | describe("Repository.status") { 772 | it("Should accurately report status for repositories with no status") { 773 | let expectedCount = 0 774 | 775 | let repo = Fixtures.mantleRepository 776 | let branch = repo.localBranch(named: "master").value! 777 | expect(repo.checkout(branch, strategy: CheckoutStrategy.None).error).to(beNil()) 778 | 779 | let status = repo.status() 780 | 781 | expect(status.value?.count).to(equal(expectedCount)) 782 | } 783 | 784 | it("Should accurately report status for repositories with status") { 785 | let expectedCount = 5 786 | let expectedNewFilePaths = [ 787 | "stage-file-1", 788 | "stage-file-2", 789 | "stage-file-3", 790 | "stage-file-4", 791 | "stage-file-5", 792 | ] 793 | let expectedOldFilePaths = [ 794 | "stage-file-1", 795 | "stage-file-2", 796 | "stage-file-3", 797 | "stage-file-4", 798 | "stage-file-5", 799 | ] 800 | 801 | let repoWithStatus = Fixtures.sharedInstance.repository(named: "repository-with-status") 802 | let branchWithStatus = repoWithStatus.localBranch(named: "master").value! 803 | expect(repoWithStatus.checkout(branchWithStatus, strategy: CheckoutStrategy.None).error).to(beNil()) 804 | 805 | let statuses = repoWithStatus.status().value! 806 | 807 | var newFilePaths: [String] = [] 808 | for status in statuses { 809 | newFilePaths.append((status.headToIndex?.newFile?.path)!) 810 | } 811 | var oldFilePaths: [String] = [] 812 | for status in statuses { 813 | oldFilePaths.append((status.headToIndex?.oldFile?.path)!) 814 | } 815 | 816 | expect(statuses.count).to(equal(expectedCount)) 817 | expect(newFilePaths).to(equal(expectedNewFilePaths)) 818 | expect(oldFilePaths).to(equal(expectedOldFilePaths)) 819 | } 820 | } 821 | 822 | describe("Repository.diff") { 823 | it("Should have accurate delta information") { 824 | let expectedCount = 13 825 | let expectedNewFilePaths = [ 826 | ".gitmodules", 827 | "Cartfile", 828 | "Cartfile.lock", 829 | "Cartfile.private", 830 | "Cartfile.resolved", 831 | "Carthage.checkout/Nimble", 832 | "Carthage.checkout/Quick", 833 | "Carthage.checkout/xcconfigs", 834 | "Carthage/Checkouts/Nimble", 835 | "Carthage/Checkouts/Quick", 836 | "Carthage/Checkouts/xcconfigs", 837 | "Mantle.xcodeproj/project.pbxproj", 838 | "Mantle.xcworkspace/contents.xcworkspacedata", 839 | ] 840 | let expectedOldFilePaths = [ 841 | ".gitmodules", 842 | "Cartfile", 843 | "Cartfile.lock", 844 | "Cartfile.private", 845 | "Cartfile.resolved", 846 | "Carthage.checkout/Nimble", 847 | "Carthage.checkout/Quick", 848 | "Carthage.checkout/xcconfigs", 849 | "Carthage/Checkouts/Nimble", 850 | "Carthage/Checkouts/Quick", 851 | "Carthage/Checkouts/xcconfigs", 852 | "Mantle.xcodeproj/project.pbxproj", 853 | "Mantle.xcworkspace/contents.xcworkspacedata", 854 | ] 855 | 856 | let repo = Fixtures.mantleRepository 857 | let branch = repo.localBranch(named: "master").value! 858 | expect(repo.checkout(branch, strategy: CheckoutStrategy.None).error).to(beNil()) 859 | 860 | let head = repo.HEAD().value! 861 | let commit = repo.object(head.oid).value! as! Commit 862 | let diff = repo.diff(for: commit).value! 863 | 864 | let newFilePaths = diff.deltas.map { $0.newFile!.path } 865 | let oldFilePaths = diff.deltas.map { $0.oldFile!.path } 866 | 867 | expect(diff.deltas.count).to(equal(expectedCount)) 868 | expect(newFilePaths).to(equal(expectedNewFilePaths)) 869 | expect(oldFilePaths).to(equal(expectedOldFilePaths)) 870 | } 871 | 872 | it("Should handle initial commit well") { 873 | let expectedCount = 2 874 | let expectedNewFilePaths = [ 875 | ".gitignore", 876 | "README.md", 877 | ] 878 | let expectedOldFilePaths = [ 879 | ".gitignore", 880 | "README.md", 881 | ] 882 | 883 | let repo = Fixtures.mantleRepository 884 | expect(repo.checkout(OID(string: "047b931bd7f5478340cef5885a6fff713005f4d6")!, 885 | strategy: CheckoutStrategy.None).error).to(beNil()) 886 | let head = repo.HEAD().value! 887 | let initalCommit = repo.object(head.oid).value! as! Commit 888 | let diff = repo.diff(for: initalCommit).value! 889 | 890 | var newFilePaths: [String] = [] 891 | for delta in diff.deltas { 892 | newFilePaths.append((delta.newFile?.path)!) 893 | } 894 | var oldFilePaths: [String] = [] 895 | for delta in diff.deltas { 896 | oldFilePaths.append((delta.oldFile?.path)!) 897 | } 898 | 899 | expect(diff.deltas.count).to(equal(expectedCount)) 900 | expect(newFilePaths).to(equal(expectedNewFilePaths)) 901 | expect(oldFilePaths).to(equal(expectedOldFilePaths)) 902 | } 903 | 904 | it("Should handle merge commits well") { 905 | let expectedCount = 20 906 | let expectedNewFilePaths = [ 907 | "Mantle.xcodeproj/project.pbxproj", 908 | "Mantle/MTLModel+NSCoding.m", 909 | "Mantle/Mantle.h", 910 | "Mantle/NSArray+MTLHigherOrderAdditions.h", 911 | "Mantle/NSArray+MTLHigherOrderAdditions.m", 912 | "Mantle/NSArray+MTLManipulationAdditions.m", 913 | "Mantle/NSDictionary+MTLHigherOrderAdditions.h", 914 | "Mantle/NSDictionary+MTLHigherOrderAdditions.m", 915 | "Mantle/NSDictionary+MTLManipulationAdditions.m", 916 | "Mantle/NSNotificationCenter+MTLWeakReferenceAdditions.h", 917 | "Mantle/NSNotificationCenter+MTLWeakReferenceAdditions.m", 918 | "Mantle/NSOrderedSet+MTLHigherOrderAdditions.h", 919 | "Mantle/NSOrderedSet+MTLHigherOrderAdditions.m", 920 | "Mantle/NSSet+MTLHigherOrderAdditions.h", 921 | "Mantle/NSSet+MTLHigherOrderAdditions.m", 922 | "Mantle/NSValueTransformer+MTLPredefinedTransformerAdditions.m", 923 | "MantleTests/MTLHigherOrderAdditionsSpec.m", 924 | "MantleTests/MTLNotificationCenterAdditionsSpec.m", 925 | "MantleTests/MTLPredefinedTransformerAdditionsSpec.m", 926 | "README.md", 927 | ] 928 | let expectedOldFilePaths = [ 929 | "Mantle.xcodeproj/project.pbxproj", 930 | "Mantle/MTLModel+NSCoding.m", 931 | "Mantle/Mantle.h", 932 | "Mantle/NSArray+MTLHigherOrderAdditions.h", 933 | "Mantle/NSArray+MTLHigherOrderAdditions.m", 934 | "Mantle/NSArray+MTLManipulationAdditions.m", 935 | "Mantle/NSDictionary+MTLHigherOrderAdditions.h", 936 | "Mantle/NSDictionary+MTLHigherOrderAdditions.m", 937 | "Mantle/NSDictionary+MTLManipulationAdditions.m", 938 | "Mantle/NSNotificationCenter+MTLWeakReferenceAdditions.h", 939 | "Mantle/NSNotificationCenter+MTLWeakReferenceAdditions.m", 940 | "Mantle/NSOrderedSet+MTLHigherOrderAdditions.h", 941 | "Mantle/NSOrderedSet+MTLHigherOrderAdditions.m", 942 | "Mantle/NSSet+MTLHigherOrderAdditions.h", 943 | "Mantle/NSSet+MTLHigherOrderAdditions.m", 944 | "Mantle/NSValueTransformer+MTLPredefinedTransformerAdditions.m", 945 | "MantleTests/MTLHigherOrderAdditionsSpec.m", 946 | "MantleTests/MTLNotificationCenterAdditionsSpec.m", 947 | "MantleTests/MTLPredefinedTransformerAdditionsSpec.m", 948 | "README.md", 949 | ] 950 | 951 | let repo = Fixtures.mantleRepository 952 | expect(repo.checkout(OID(string: "d0d9c13da5eb5f9e8cf2a9f1f6ca3bdbe975b57d")!, 953 | strategy: CheckoutStrategy.None).error).to(beNil()) 954 | let head = repo.HEAD().value! 955 | let initalCommit = repo.object(head.oid).value! as! Commit 956 | let diff = repo.diff(for: initalCommit).value! 957 | 958 | var newFilePaths: [String] = [] 959 | for delta in diff.deltas { 960 | newFilePaths.append((delta.newFile?.path)!) 961 | } 962 | var oldFilePaths: [String] = [] 963 | for delta in diff.deltas { 964 | oldFilePaths.append((delta.oldFile?.path)!) 965 | } 966 | 967 | expect(diff.deltas.count).to(equal(expectedCount)) 968 | expect(newFilePaths).to(equal(expectedNewFilePaths)) 969 | expect(oldFilePaths).to(equal(expectedOldFilePaths)) 970 | } 971 | } 972 | } 973 | 974 | func temporaryURL(forPurpose purpose: String) -> URL { 975 | let globallyUniqueString = ProcessInfo.processInfo.globallyUniqueString 976 | return FileManager.default 977 | .temporaryDirectory 978 | .appendingPathComponent("\(globallyUniqueString)_\(purpose)") 979 | } 980 | } 981 | --------------------------------------------------------------------------------