diff --git a/SwiftGit2.xcodeproj/project.pbxproj b/SwiftGit2.xcodeproj/project.pbxproj index 6c6d0b77..afa5d830 100644 --- a/SwiftGit2.xcodeproj/project.pbxproj +++ b/SwiftGit2.xcodeproj/project.pbxproj @@ -21,6 +21,9 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 232861431F4A3A2E00276D65 /* Diffs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 232861421F4A3A2E00276D65 /* Diffs.swift */; }; + 232861451F4A3A2E00276D65 /* Diffs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 232861421F4A3A2E00276D65 /* Diffs.swift */; }; + 237731C71F46542B0020A3FE /* repository-with-status.zip in Resources */ = {isa = PBXBuildFile; fileRef = 237731C61F46542B0020A3FE /* repository-with-status.zip */; }; 2549921B34FFC36AF8C9CD6D /* CommitIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25499A996CA7BD416620A397 /* CommitIterator.swift */; }; 25499D325997CAB9BEFFCA4D /* CommitIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25499A996CA7BD416620A397 /* CommitIterator.swift */; }; 621E66A01C72958800A0F352 /* OID.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE70B3E41A1ACB1A002C3F4E /* OID.swift */; }; @@ -129,6 +132,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 232861421F4A3A2E00276D65 /* Diffs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Diffs.swift; sourceTree = ""; }; + 237731C61F46542B0020A3FE /* repository-with-status.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = "repository-with-status.zip"; sourceTree = ""; }; 25499A996CA7BD416620A397 /* CommitIterator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommitIterator.swift; sourceTree = ""; }; 621E66B41C72958800A0F352 /* SwiftGit2.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftGit2.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 621E66CE1C72958D00A0F352 /* SwiftGit2-iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SwiftGit2-iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -260,6 +265,7 @@ BE14AA531A1983520015B439 /* Fixtures */ = { isa = PBXGroup; children = ( + 237731C61F46542B0020A3FE /* repository-with-status.zip */, BE0B1C5C1A9978890004726D /* detached-head.zip */, BE14AA541A1984550015B439 /* Fixtures.swift */, BE0991F61A578FB1007D4E6A /* Mantle.zip */, @@ -272,6 +278,7 @@ isa = PBXGroup; children = ( BEB31F251A0D6F7A00F525B9 /* SwiftGit2 */, + BEB31F261A0D6F7A00F525B9 /* Supporting Files */, BEB31F321A0D6F7A00F525B9 /* SwiftGit2Tests */, BEB31FA11A0E63C100F525B9 /* Libraries */, BEB31F411A0D75EE00F525B9 /* Configuration */, @@ -305,12 +312,12 @@ DA5914751A94579000AED74C /* Errors.swift */, BE36354B1A632C9700D37EC8 /* Libgit2.swift */, BE2E3BE51A31261300C67092 /* Objects.swift */, + 232861421F4A3A2E00276D65 /* Diffs.swift */, BE70B3E41A1ACB1A002C3F4E /* OID.swift */, BE7A753E1A4A2BCC002DA7E3 /* Pointers.swift */, BEB31F6C1A0D78F300F525B9 /* Repository.swift */, BECB5F691A56F19900999413 /* References.swift */, BECB5F6D1A57284700999413 /* Remotes.swift */, - BEB31F261A0D6F7A00F525B9 /* Supporting Files */, 25499A996CA7BD416620A397 /* CommitIterator.swift */, ); path = SwiftGit2; @@ -322,6 +329,7 @@ BEB31F271A0D6F7A00F525B9 /* Info.plist */, ); name = "Supporting Files"; + path = SwiftGit2; sourceTree = ""; }; BEB31F321A0D6F7A00F525B9 /* SwiftGit2Tests */ = { @@ -655,6 +663,7 @@ buildActionMask = 2147483647; files = ( BE0B1C5D1A9978890004726D /* detached-head.zip in Resources */, + 237731C71F46542B0020A3FE /* repository-with-status.zip in Resources */, BE0991F71A578FB1007D4E6A /* Mantle.zip in Resources */, BE14AA571A198C6E0015B439 /* simple-repository.zip in Resources */, ); @@ -742,6 +751,7 @@ 622726351C84E52500C53D17 /* Credentials.swift in Sources */, 621E66A31C72958800A0F352 /* Repository.swift in Sources */, 621E66A41C72958800A0F352 /* Objects.swift in Sources */, + 232861451F4A3A2E00276D65 /* Diffs.swift in Sources */, 621E66A51C72958800A0F352 /* References.swift in Sources */, 621E66A61C72958800A0F352 /* Libgit2.swift in Sources */, 621E66A71C72958800A0F352 /* Pointers.swift in Sources */, @@ -775,6 +785,7 @@ 622726341C84E52500C53D17 /* Credentials.swift in Sources */, BEB31F6D1A0D78F300F525B9 /* Repository.swift in Sources */, BE2E3BE61A31261300C67092 /* Objects.swift in Sources */, + 232861431F4A3A2E00276D65 /* Diffs.swift in Sources */, BECB5F6A1A56F19900999413 /* References.swift in Sources */, BE36354C1A632C9700D37EC8 /* Libgit2.swift in Sources */, BE7A753F1A4A2BCC002DA7E3 /* Pointers.swift in Sources */, @@ -1008,6 +1019,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "org.libgit2.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = SwiftGit2; SWIFT_INCLUDE_PATHS = "$(SRCROOT)/libgit2"; + SWIFT_VERSION = 4.0; }; name = Debug; }; @@ -1036,6 +1048,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "org.libgit2.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = SwiftGit2; SWIFT_INCLUDE_PATHS = "$(SRCROOT)/libgit2"; + SWIFT_VERSION = 4.0; }; name = Release; }; diff --git a/SwiftGit2/CommitIterator.swift b/SwiftGit2/CommitIterator.swift index 88cdeda9..eb701bcc 100644 --- a/SwiftGit2/CommitIterator.swift +++ b/SwiftGit2/CommitIterator.swift @@ -10,11 +10,11 @@ public class CommitIterator: IteratorProtocol, Sequence { public typealias Iterator = CommitIterator public typealias Element = Result let repo: Repository - private var revisionWalker: OpaquePointer? = nil + private var revisionWalker: OpaquePointer? private enum Next { case over - case ok + case okay case error(NSError) init(_ result: Int32, name: String) { @@ -22,7 +22,7 @@ public class CommitIterator: IteratorProtocol, Sequence { case GIT_ITEROVER.rawValue: self = .over case GIT_OK.rawValue: - self = .ok + self = .okay default: self = .error(NSError(gitError: result, pointOfFailure: name)) } @@ -55,7 +55,7 @@ public class CommitIterator: IteratorProtocol, Sequence { return Result.failure(error) case .over: return nil - case .ok: + case .okay: var unsafeCommit: OpaquePointer? = nil let lookupGitResult = git_commit_lookup(&unsafeCommit, repo.pointer, &oid) guard lookupGitResult == GIT_OK.rawValue, @@ -76,7 +76,7 @@ public class CommitIterator: IteratorProtocol, Sequence { public func map(_ transform: (Result) throws -> T) rethrows -> [T] { var new: [T] = [] for item in self { - new = new + [try transform(item)] + new += [try transform(item)] } return new } @@ -85,7 +85,7 @@ public class CommitIterator: IteratorProtocol, Sequence { var new: [Result] = [] for item in self { if try isIncluded(item) { - new = new + [item] + new += [item] } } return new @@ -104,12 +104,12 @@ public class CommitIterator: IteratorProtocol, Sequence { self.repo = repo } - public func dropFirst(_ n: Int) -> AnySequence { + public func dropFirst(_ num: Int) -> AnySequence { notImplemented(functionName: self.dropFirst) return AnySequence { return CommitIterator(repo: self.repo) } } - public func dropLast(_ n: Int) -> AnySequence { + public func dropLast(_ num: Int) -> AnySequence { notImplemented(functionName: self.dropLast) return AnySequence { return CommitIterator(repo: self.repo) } } diff --git a/SwiftGit2/Diffs.swift b/SwiftGit2/Diffs.swift new file mode 100644 index 00000000..7caf8d88 --- /dev/null +++ b/SwiftGit2/Diffs.swift @@ -0,0 +1,111 @@ +// +// Diffs.swift +// SwiftGit2 +// +// Created by Jake Van Alstyne on 8/20/17. +// Copyright © 2017 GitHub, Inc. All rights reserved. +// +import Foundation +import libgit2 + +public struct StatusEntry { + public var status: Diff.Status + public var headToIndex: Diff.Delta? + public var indexToWorkDir: Diff.Delta? + + public init(from statusEntry: git_status_entry) { + self.status = Diff.Status(rawValue: statusEntry.status.rawValue) + + if let htoi = statusEntry.head_to_index { + self.headToIndex = Diff.Delta(htoi.pointee) + } + + if let itow = statusEntry.index_to_workdir { + self.indexToWorkDir = Diff.Delta(itow.pointee) + } + } +} + +public struct Diff { + + /// The set of deltas. + public var deltas = [Delta]() + + public struct Delta { + public static let type = GIT_OBJ_REF_DELTA + + public var status: Status + public var flags: Flags + public var oldFile: File? + public var newFile: File? + + public init(_ delta: git_diff_delta) { + self.status = Status(rawValue: UInt32(git_diff_status_char(delta.status))) + self.flags = Flags(rawValue: delta.flags) + self.oldFile = File(delta.old_file) + self.newFile = File(delta.new_file) + } + } + + public struct File { + public var oid: OID + public var path: String + public var size: Int64 + public var flags: Flags + + public init(_ diffFile: git_diff_file) { + self.oid = OID(diffFile.id) + let path = diffFile.path + self.path = path.map(String.init(cString:))! + self.size = diffFile.size + self.flags = Flags(rawValue: diffFile.flags) + } + } + + public struct Status: OptionSet { + // This appears to be necessary due to bug in Swift + // https://bugs.swift.org/browse/SR-3003 + public init(rawValue: UInt32) { + self.rawValue = rawValue + } + public let rawValue: UInt32 + + public static let current = Status(rawValue: 0) + public static let indexNew = Status(rawValue: 1 << 0) + public static let indexModified = Status(rawValue: 1 << 1) + public static let indexDeleted = Status(rawValue: 1 << 2) + public static let indexRenamed = Status(rawValue: 1 << 3) + public static let indexTypeChange = Status(rawValue: 1 << 4) + public static let workTreeNew = Status(rawValue: 1 << 5) + public static let workTreeModified = Status(rawValue: 1 << 6) + public static let workTreeDeleted = Status(rawValue: 1 << 7) + public static let workTreeTypeChange = Status(rawValue: 1 << 8) + public static let workTreeRenamed = Status(rawValue: 1 << 9) + public static let workTreeUnreadable = Status(rawValue: 1 << 10) + public static let ignored = Status(rawValue: 1 << 11) + public static let conflicted = Status(rawValue: 1 << 12) + } + + public struct Flags: OptionSet { + // This appears to be necessary due to bug in Swift + // https://bugs.swift.org/browse/SR-3003 + public init(rawValue: UInt32) { + self.rawValue = rawValue + } + public let rawValue: UInt32 + + public static let binary = Flags(rawValue: 0) + public static let notBinary = Flags(rawValue: 1 << 0) + public static let validId = Flags(rawValue: 1 << 1) + public static let exists = Flags(rawValue: 1 << 2) + } + + /// Create an instance with a libgit2 `git_diff`. + public init(_ pointer: OpaquePointer) { + for i in 0..(_ oids: [OID], type: git_otype, transform: ([OpaquePointer]) -> Result) -> Result { + var pointers = [OpaquePointer]() + defer { + for pointer in pointers { + git_object_free(pointer) + } + } + + for oid in oids { + var pointer: OpaquePointer? = nil + var oid = oid.oid + let result = git_object_lookup(&pointer, self.pointer, &oid, type) + + guard result == GIT_OK.rawValue else { + return Result.failure(NSError(gitError: result, pointOfFailure: "git_object_lookup")) + } + + pointers.append(pointer!) + } + + return transform(pointers) + } + /// Loads the object with the given OID. /// /// oid - The OID of the blob to look up. @@ -347,13 +370,14 @@ final public class Repository { private func remoteLookup(named name: String, _ callback: (Result) -> A) -> A { var pointer: OpaquePointer? = nil + defer { git_remote_free(pointer) } + let result = git_remote_lookup(&pointer, self.pointer, name) guard result == GIT_OK.rawValue else { return callback(.failure(NSError(gitError: result, pointOfFailure: "git_remote_lookup"))) } - defer { git_remote_free(pointer) } return callback(.success(pointer!)) } @@ -379,7 +403,7 @@ final public class Repository { let err = NSError(gitError: result, pointOfFailure: "git_remote_fetch") return .failure(err) } - return .success() + return .success(()) } } } @@ -497,7 +521,7 @@ final public class Repository { guard result == GIT_OK.rawValue else { return Result.failure(NSError(gitError: result, pointOfFailure: "git_repository_set_head")) } - return Result.success() + return Result.success(()) } /// Set HEAD to the given reference. @@ -509,7 +533,7 @@ final public class Repository { guard result == GIT_OK.rawValue else { return Result.failure(NSError(gitError: result, pointOfFailure: "git_repository_set_head")) } - return Result.success() + return Result.success(()) } /// Check out HEAD. @@ -525,7 +549,7 @@ final public class Repository { return Result.failure(NSError(gitError: result, pointOfFailure: "git_checkout_head")) } - return Result.success() + return Result.success(()) } /// Check out the given OID. @@ -558,4 +582,235 @@ final public class Repository { let iterator = CommitIterator(repo: self, root: branch.oid.oid) return iterator } + + // MARK: - Diffs + + public func diff(for commit: Commit) -> Result { + typealias Delta = Diff.Delta + + guard !commit.parents.isEmpty else { + // Initial commit in a repository + return self.diff(from: nil, to: commit.oid) + } + + var mergeDiff: OpaquePointer? = nil + defer { git_object_free(mergeDiff) } + for parent in commit.parents { + let error = self.diff(from: parent.oid, to: commit.oid) { (diff: Result) -> NSError? in + guard diff.error == nil else { + return diff.error! + } + + if mergeDiff == nil { + mergeDiff = diff.value! + } else { + let mergeResult = git_diff_merge(mergeDiff, diff.value) + guard mergeResult == GIT_OK.rawValue else { + return NSError(gitError: mergeResult, pointOfFailure: "git_diff_merge") + } + } + return nil + } + + if error != nil { + return Result.failure(error!) + } + } + + return .success(Diff(mergeDiff!)) + } + + private func diff(from oldCommitOid: OID?, to newCommitOid: OID?, transform: (Result) -> NSError?) -> NSError? { + assert(oldCommitOid != nil || newCommitOid != nil, "It is an error to pass nil for both the oldOid and newOid") + + var oldTree: OpaquePointer? = nil + defer { git_object_free(oldTree) } + if let oid = oldCommitOid { + let result = unsafeTreeForCommitId(oid) + guard result.error == nil else { + return transform(Result.failure(result.error!)) + } + + oldTree = result.value + } + + var newTree: OpaquePointer? = nil + defer { git_object_free(newTree) } + if let oid = newCommitOid { + let result = unsafeTreeForCommitId(oid) + guard result.error == nil else { + return transform(Result.failure(result.error!)) + } + + newTree = result.value + } + + var diff: OpaquePointer? = nil + let diffResult = git_diff_tree_to_tree(&diff, + self.pointer, + oldTree, + newTree, + nil) + + guard diffResult == GIT_OK.rawValue else { + return transform(.failure(NSError(gitError: diffResult, + pointOfFailure: "git_diff_tree_to_tree"))) + } + + return transform(Result.success(diff!)) + } + + /// Memory safe + private func diff(from oldCommitOid: OID?, to newCommitOid: OID?) -> Result { + assert(oldCommitOid != nil || newCommitOid != nil, "It is an error to pass nil for both the oldOid and newOid") + + var oldTree: Tree? = nil + if oldCommitOid != nil { + let result = safeTreeForCommitId(oldCommitOid!) + guard result.error == nil else { + return Result.failure(result.error!) + } + oldTree = result.value + } + + var newTree: Tree? = nil + if newCommitOid != nil { + let result = self.safeTreeForCommitId(newCommitOid!) + guard result.error == nil else { + return Result.failure(result.error!) + } + newTree = result.value! + } + + if oldTree != nil && newTree != nil { + return withGitObjects([oldTree!.oid, newTree!.oid], type: GIT_OBJ_TREE) { objects in + var diff: OpaquePointer? = nil + let diffResult = git_diff_tree_to_tree(&diff, + self.pointer, + objects[0], + objects[1], + nil) + return processTreeToTreeDiff(diffResult, diff: diff) + } + } else if let tree = oldTree { + return withGitObject(tree.oid, type: GIT_OBJ_TREE, transform: { tree in + var diff: OpaquePointer? = nil + let diffResult = git_diff_tree_to_tree(&diff, + self.pointer, + tree, + nil, + nil) + return processTreeToTreeDiff(diffResult, diff: diff) + }) + } else if let tree = newTree { + return withGitObject(tree.oid, type: GIT_OBJ_TREE, transform: { tree in + var diff: OpaquePointer? = nil + let diffResult = git_diff_tree_to_tree(&diff, + self.pointer, + nil, + tree, + nil) + return processTreeToTreeDiff(diffResult, diff: diff) + }) + } + + return .failure(NSError(gitError: -1, pointOfFailure: "diff(from: to:)")) + } + + private func processTreeToTreeDiff(_ diffResult: Int32, diff: OpaquePointer?) -> Result { + guard diffResult == GIT_OK.rawValue else { + return .failure(NSError(gitError: diffResult, + pointOfFailure: "git_diff_tree_to_tree")) + } + + let diffObj = Diff(diff!) + git_diff_free(diff) + return .success(diffObj) + } + + private func processDiffDeltas(_ diffResult: OpaquePointer) -> Result<[Diff.Delta], NSError> { + typealias Delta = Diff.Delta + var returnDict = [Delta]() + + let count = git_diff_num_deltas(diffResult) + + for i in 0...success(returnDict) + return result + } + + private func safeTreeForCommitId(_ oid: OID) -> Result { + return withGitObject(oid, type: GIT_OBJ_COMMIT) { commit in + let treeId = git_commit_tree_id(commit) + let tree = self.tree(OID(treeId!.pointee)) + guard tree.error == nil else { + return .failure(tree.error!) + } + return tree + } + } + + /// Caller responsible to free returned tree with git_object_free + private func unsafeTreeForCommitId(_ oid: OID) -> Result { + var commit: OpaquePointer? = nil + var oid = oid.oid + let commitResult = git_object_lookup(&commit, self.pointer, &oid, GIT_OBJ_COMMIT) + guard commitResult == GIT_OK.rawValue else { + return .failure(NSError(gitError: commitResult, pointOfFailure: "git_object_lookup")) + } + + var tree: OpaquePointer? = nil + let treeId = git_commit_tree_id(commit) + let treeResult = git_object_lookup(&tree, self.pointer, treeId, GIT_OBJ_TREE) + + git_object_free(commit) + + guard treeResult == GIT_OK.rawValue else { + return .failure(NSError(gitError: treeResult, pointOfFailure: "git_object_lookup")) + } + + return Result.success(tree!) + } + + // MARK: - Status + + public func status() -> Result<[StatusEntry], NSError> { + var returnArray = [StatusEntry]() + + // Do this because GIT_STATUS_OPTIONS_INIT is unavailable in swift + let pointer = UnsafeMutablePointer.allocate(capacity: 1) + let optionsResult = git_status_init_options(pointer, UInt32(GIT_STATUS_OPTIONS_VERSION)) + guard optionsResult == GIT_OK.rawValue else { + return .failure(NSError(gitError: optionsResult, pointOfFailure: "git_status_init_options")) + } + var options = pointer.move() + pointer.deallocate(capacity: 1) + + var unsafeStatus: OpaquePointer? = nil + defer { git_status_list_free(unsafeStatus) } + let statusResult = git_status_list_new(&unsafeStatus, self.pointer, &options) + guard statusResult == GIT_OK.rawValue, let unwrapStatusResult = unsafeStatus else { + return .failure(NSError(gitError: statusResult, pointOfFailure: "git_status_list_new")) + } + + let count = git_status_list_entrycount(unwrapStatusResult) + + for i in 0.. URL {