實(shí)現(xiàn)原理是在客戶端應(yīng)用里通過shell執(zhí)行g(shù)it相關(guān)命令
需要一個(gè)Shell管理類:ShellClient.swift
import Foundation
import Combine
public struct ShellClient {
public var runLive: (_ args: String...) -> AnyPublisher<String, Never>
public var run: (_ args: String...) throws -> String
public init(
runLive: @escaping (_ args: String...) -> AnyPublisher<String, Never>,
run: @escaping (_ args: String...) throws -> String
) {
self.runLive = runLive
self.run = run
}
}
public extension ShellClient {
static func live() -> Self {
func generateProcessAndPipe(_ args: [String]) -> (Process, Pipe) {
var arguments = ["-c"]
arguments.append(contentsOf: args)
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.arguments = arguments
task.executableURL = URL(fileURLWithPath: "/bin/zsh")
return (task, pipe)
}
var cancellables: [UUID: AnyCancellable] = [:]
func runLive(_ args: String...) -> AnyPublisher<String, Never> {
let subject = PassthroughSubject<String, Never>()
let (task, pipe) = generateProcessAndPipe(args)
let outputHandler = pipe.fileHandleForReading
outputHandler.waitForDataInBackgroundAndNotify()
let id = UUID()
cancellables[id] = NotificationCenter
.default
.publisher(for: .NSFileHandleDataAvailable, object: outputHandler)
.sink { _ in
let data = outputHandler.availableData
guard data.count > 0 else {
cancellables.removeValue(forKey: id)
subject.send(completion: .finished)
return
}
if let line = String(data: data, encoding: .utf8)?
.split(whereSeparator: \.isNewline) {
line
.map(String.init)
.forEach(subject.send(_:))
}
outputHandler.waitForDataInBackgroundAndNotify()
}
task.launch()
return subject.eraseToAnyPublisher()
}
func run(_ args: String...) throws -> String {
let (task, pipe) = generateProcessAndPipe(args)
try task.run()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
return String(data: data, encoding: .utf8) ?? ""
}
return ShellClient(
runLive: runLive(_:),
run: run(_:)
)
}
}
需要一個(gè)Git管理類:GitClient.swift
import Foundation
import ShellClient
public struct GitClient {
public var getCurrentBranchName: () throws -> String
public var getBranches: (Bool) throws -> [String]
public var checkoutBranch: (String) throws -> Void
init(
getCurrentBranchName: @escaping () throws -> String,
getBranches: @escaping (Bool) throws -> [String],
checkoutBranch: @escaping (String) throws -> Void
) {
self.getCurrentBranchName = getCurrentBranchName
self.getBranches = getBranches
self.checkoutBranch = checkoutBranch
}
public enum GitClientError: Error {
case outputError(String)
case notGitRepository
case failedToDecodeURL
}
public static func `default`(
directoryURL: URL,
shellClient: ShellClient
) -> GitClient {
// 獲取當(dāng)前分支名稱
func getCurrentBranchName() throws -> String {
let output = try shellClient.run(
"cd \(directoryURL.relativePath.escapedWhiteSpaces());git rev-parse --abbrev-ref HEAD"
)
.replacingOccurrences(of: "\n", with: "")
if output.contains("fatal: not a git repository") {
throw GitClientError.notGitRepository
}
return output
}
// 參考 https://git-scm.com/docs/git-branch --format參數(shù)同 https://git-scm.com/docs/git-for-each-ref
//git branch --sort committerdate --format '%(committerdate:short) %09 %(authorname) %09 %(refname:short) %09 %(objectname:short=7) %09 %(committer)'
// committer(全部)狞洋、committeremail施绎、committername栏尚、committerdate //提交人信息
// objectname:提交編號(hào)
//
// 獲取分支列表害淤,這里只獲取分支名稱赂苗,其他信息參考上面信息
func getBranches(_ allBranches: Bool = false) throws -> [String] {
if allBranches == true {
//本地和遠(yuǎn)程所有分支(remotes/開頭的表示遠(yuǎn)程分支)
return try shellClient.run(
"cd \(directoryURL.relativePath.escapedWhiteSpaces());git branch -a --format \"%(refname:short)\""
)
.components(separatedBy: "\n")
.filter { $0 != "" }
}
//本地所有分支
return try shellClient.run(
"cd \(directoryURL.relativePath.escapedWhiteSpaces());git branch --format \"%(refname:short)\""
)
.components(separatedBy: "\n")
.filter { $0 != "" }
}
// 選擇分支
func checkoutBranch(name: String) throws -> Void {
guard try getCurrentBranchName() != name else { return }
let output = try shellClient.run(
"cd \(directoryURL.relativePath.escapedWhiteSpaces());git checkout \(name)"
)
if output.contains("fatal: not a git repository") {
throw GitClientError.notGitRepository
} else if !output.contains("Switched to branch") && !output.contains("Switched to a new branch") {
throw GitClientError.outputError(output)
}
}
return GitClient(
getCurrentBranchName: getCurrentBranchName,
getBranches: getBranches(_:),
checkoutBranch: checkoutBranch(name:))
}
}
private extension String {
func escapedWhiteSpaces() -> String {
self.replacingOccurrences(of: " ", with: "\\ ")
}
}
調(diào)用:
var shellClient = ShellClient.live()
var gitClient = GitClient.default(directoryURL: "本地git目錄",shellClient:shellClient)
// Git 獲取當(dāng)前分支名稱
var currentBranch = try? gitClient?.getCurrentBranchName()
//Git 獲取分支列表(除當(dāng)前分支)
var branchName:[String] {
((try? gitClient?.getBranches(false)) ?? []).filter{ $0 != currentBranch }
}
// Git切換分支
func checkBranch(_ name: String) {
//切換
try? gitClient?.checkoutBranch(name)
//重新獲取當(dāng)前分支名稱
currentBranch = try? gitClient?getCurrentBranchName()
}
最終實(shí)現(xiàn)效果
UI代碼就不貼了向挖,采用SwiftUI寫的九孩,這里只展示了分支名稱躏惋,當(dāng)然可以根據(jù)上面注釋加入提交人信息和提交編號(hào),當(dāng)然也可以加入類似Xcode一樣的過濾搜索框咙。Popover SwiftUI 采用
.popover(isPresented:$“@State修飾變量”, arrowEdge:.bottom)