-
-
Notifications
You must be signed in to change notification settings - Fork 106
/
Copy pathGitRepository.swift
179 lines (160 loc) · 7.52 KB
/
GitRepository.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
//
// GitRepository.swift
// pass
//
// Created by Mingshen Sun on 1/25/25.
// Copyright © 2025 Bob Sun. All rights reserved.
//
import ObjectiveGit
public typealias TransferProgressHandler = (UnsafePointer<git_transfer_progress>, UnsafeMutablePointer<ObjCBool>) -> Void
public typealias CheckoutProgressHandler = (String, UInt, UInt) -> Void
public typealias PushProgressHandler = (UInt32, UInt32, Int, UnsafeMutablePointer<ObjCBool>) -> Void
public typealias CloneOptions = [AnyHashable: Any]
public typealias PullOptions = [AnyHashable: Any]
public typealias PushOptions = [String: Any]
public class GitRepository {
let repository: GTRepository
var branchName: String = "master"
public init(with localDir: URL) throws {
guard FileManager.default.fileExists(atPath: localDir.path) else {
throw AppError.repositoryNotSet
}
try self.repository = GTRepository(url: localDir)
if let currentBranchName = try? repository.currentBranch().name {
self.branchName = currentBranchName
}
}
public init(from remoteURL: URL, to workingDir: URL, branchName: String, options: CloneOptions, transferProgressBlock: @escaping TransferProgressHandler, checkoutProgressBlock: @escaping CheckoutProgressHandler) throws {
self.repository = try GTRepository.clone(
from: remoteURL,
toWorkingDirectory: workingDir,
options: options,
transferProgressBlock: transferProgressBlock
)
self.branchName = branchName
guard !repository.isHEADUnborn else {
return
}
if (try repository.currentBranch().name) != branchName {
try checkoutAndChangeBranch(branchName: branchName, progressBlock: checkoutProgressBlock)
}
}
public func checkoutAndChangeBranch(branchName: String, progressBlock: @escaping CheckoutProgressHandler) throws {
self.branchName = branchName
if let localBranch = try? repository.lookUpBranch(withName: branchName, type: .local, success: nil) {
let checkoutOptions = GTCheckoutOptions(strategy: .force, progressBlock: progressBlock)
try repository.checkoutReference(localBranch.reference, options: checkoutOptions)
try repository.moveHEAD(to: localBranch.reference)
} else {
let remoteBranchName = "origin/\(branchName)"
let remoteBranch = try repository.lookUpBranch(withName: remoteBranchName, type: .remote, success: nil)
guard let remoteBranchOid = remoteBranch.oid else {
throw AppError.repositoryRemoteBranchNotFound(branchName: remoteBranchName)
}
let localBranch = try repository.createBranchNamed(branchName, from: remoteBranchOid, message: nil)
try localBranch.updateTrackingBranch(remoteBranch)
let checkoutOptions = GTCheckoutOptions(strategy: .force, progressBlock: progressBlock)
try repository.checkoutReference(localBranch.reference, options: checkoutOptions)
try repository.moveHEAD(to: localBranch.reference)
}
}
public func pull(
options: PullOptions,
transferProgressBlock: @escaping TransferProgressHandler
) throws {
let remote = try GTRemote(name: "origin", in: repository)
try repository.pull(repository.currentBranch(), from: remote, withOptions: options, progress: transferProgressBlock)
}
public func getRecentCommits(count: Int) throws -> [GTCommit] {
var commits = [GTCommit]()
let enumerator = try GTEnumerator(repository: repository)
if let targetOID = try repository.headReference().targetOID {
try enumerator.pushSHA(targetOID.sha)
}
for _ in 0 ..< count {
if let commit = try? enumerator.nextObject(withSuccess: nil) {
commits.append(commit)
}
}
return commits
}
public func add(path: String) throws {
try repository.index().addFile(path)
try repository.index().write()
}
public func rm(path: String) throws {
guard let repoURL = repository.fileURL else {
throw AppError.repositoryNotSet
}
let url = repoURL.appendingPathComponent(path)
if FileManager.default.fileExists(atPath: url.path) {
try FileManager.default.removeItem(at: url)
}
try repository.index().removeFile(path)
try repository.index().write()
}
public func mv(from: String, to: String) throws {
guard let repoURL = repository.fileURL else {
throw AppError.repositoryNotSet
}
let fromURL = repoURL.appendingPathComponent(from)
let toURL = repoURL.appendingPathComponent(to)
try FileManager.default.moveItem(at: fromURL, to: toURL)
try add(path: to)
try rm(path: from)
}
public func commit(name: String, email: String, message: String) throws -> GTCommit {
guard let signature = GTSignature(name: name, email: email, time: Date()) else {
throw AppError.gitCreateSignature
}
return try commit(signature: signature, message: message)
}
public func commit(signature: GTSignature, message: String) throws -> GTCommit {
let newTree = try repository.index().writeTree()
if repository.isHEADUnborn {
return try repository.createCommit(with: newTree, message: message, author: signature, committer: signature, parents: nil, updatingReferenceNamed: "HEAD")
}
let headReference = try repository.headReference()
let commitEnum = try GTEnumerator(repository: repository)
try commitEnum.pushSHA(headReference.targetOID!.sha)
guard let parent = commitEnum.nextObject() as? GTCommit else {
throw AppError.gitCommit
}
return try repository.createCommit(with: newTree, message: message, author: signature, committer: signature, parents: [parent], updatingReferenceNamed: headReference.name)
}
public func push(
options: [String: Any],
transferProgressBlock: @escaping PushProgressHandler
) throws {
let branch = try repository.currentBranch()
let remote = try GTRemote(name: "origin", in: repository)
try repository.push(branch, to: remote, withOptions: options, progress: transferProgressBlock)
}
public func getLocalCommits() throws -> [GTCommit] {
let remoteBranchName = "origin/\(branchName)"
let remoteBranch = try repository.lookUpBranch(withName: remoteBranchName, type: .remote, success: nil)
return try repository.localCommitsRelative(toRemoteBranch: remoteBranch)
}
public func numberOfCommits() -> Int {
Int(repository.numberOfCommits(inCurrentBranch: nil))
}
public func reset() throws {
let localCommits = try getLocalCommits()
if localCommits.isEmpty {
return
}
guard let firstLocalCommit = localCommits.last,
firstLocalCommit.parents.count == 1,
let newHead = firstLocalCommit.parents.first else {
throw AppError.gitReset
}
try repository.reset(to: newHead, resetType: .hard)
}
public func lastCommitDate(path: String) throws -> Date {
let blameHunks = try repository.blame(withFile: path, options: nil).hunks
guard let latestCommitTime = blameHunks.map({ $0.finalSignature?.time?.timeIntervalSince1970 ?? 0 }).max() else {
return Date(timeIntervalSince1970: 0)
}
return Date(timeIntervalSince1970: latestCommitTime)
}
}