实现原理是在客户端应用里通过shell执行git相关命令
需要一个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(_:)
)
}
}
需要一个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 {
// 获取当前分支名称
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参数同 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:提交编号
//
// 获取分支列表,这里只获取分支名称,其他信息参考上面信息
func getBranches(_ allBranches: Bool = false) throws -> [String] {
if allBranches == true {
//本地和远程所有分支(remotes/开头的表示远程分支)
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: "\\ ")
}
}
调用:
var shellClient = ShellClient.live()
var gitClient = GitClient.default(directoryURL: "本地git目录",shellClient:shellClient)
// Git 获取当前分支名称
var currentBranch = try? gitClient?.getCurrentBranchName()
//Git 获取分支列表(除当前分支)
var branchName:[String] {
((try? gitClient?.getBranches(false)) ?? []).filter{ $0 != currentBranch }
}
// Git切换分支
func checkBranch(_ name: String) {
//切换
try? gitClient?.checkoutBranch(name)
//重新获取当前分支名称
currentBranch = try? gitClient?getCurrentBranchName()
}
最终实现效果
UI代码就不贴了,采用SwiftUI写的,这里只展示了分支名称,当然可以根据上面注释加入提交人信息和提交编号,当然也可以加入类似Xcode一样的过滤搜索。Popover SwiftUI 采用
.popover(isPresented:$“@State修饰变量”, arrowEdge:.bottom)