127 lines
5.3 KiB
Swift
127 lines
5.3 KiB
Swift
import UIKit
|
|
|
|
/// Builds and launches Working Copy x-callback-url commands.
|
|
/// Works with any remote — GitHub, local bare repo over SSH via Tailscale,
|
|
/// Gitea, etc. The remote URL is managed entirely inside Working Copy;
|
|
/// PadXcode only needs the repo display name.
|
|
final class GitService {
|
|
|
|
// Read from UserDefaults at call time so changes saved in Settings
|
|
// are reflected immediately without requiring an app restart.
|
|
private var apiKey: String {
|
|
UserDefaults.standard.string(forKey: "workingCopyAPIKey") ?? ""
|
|
}
|
|
private let callbackBase = "padxcode://git-callback"
|
|
|
|
init() {}
|
|
|
|
// MARK: - Navigation
|
|
|
|
func listRepositories() { open(build("repos", [:])) }
|
|
|
|
func fetchStatus(repo: String) {
|
|
open(build("status", ["repo": repo, "unchanged": "1"], success: "status"))
|
|
}
|
|
func fetchLog(repo: String, limit: Int = 30) {
|
|
open(build("log", ["repo": repo, "limit": "\(limit)"], success: "log"))
|
|
}
|
|
func fetchBranches(repo: String) {
|
|
open(build("branches", ["repo": repo], success: "branches"))
|
|
}
|
|
|
|
// MARK: - Branch operations
|
|
|
|
func checkout(repo: String, branch: String) {
|
|
open(build("checkout", ["repo": repo, "branch": branch], success: "checkout"))
|
|
}
|
|
func createBranch(repo: String, branch: String) {
|
|
open(build("checkout", ["repo": repo, "branch": branch, "mode": "create"], success: "checkout"))
|
|
}
|
|
func merge(repo: String, branch: String) {
|
|
open(build("merge", ["repo": repo, "branch": branch], success: "merge"))
|
|
}
|
|
|
|
// MARK: - Commit / Push / Pull
|
|
|
|
func commit(repo: String, message: String, path: String = "", limit: Int = 999) {
|
|
var p: [String: String] = ["repo": repo, "message": message, "limit": "\(limit)"]
|
|
if !path.isEmpty { p["path"] = path }
|
|
open(build("commit", p, success: "commit"))
|
|
}
|
|
func push(repo: String) { open(build("push", ["repo": repo], success: "push")) }
|
|
func pull(repo: String) { open(build("pull", ["repo": repo], success: "pull")) }
|
|
func fetch(repo: String) { open(build("fetch", ["repo": repo], success: "fetch")) }
|
|
|
|
/// Commit then push in one Working Copy session using the `chain` command.
|
|
/// This is the primary action used by the Git panel Commit & Push button.
|
|
func commitAndPush(repo: String, message: String, path: String = "") {
|
|
var components = URLComponents(string: "working-copy://x-callback-url/chain")!
|
|
var items: [URLQueryItem] = [
|
|
.init(name: "key", value: apiKey),
|
|
.init(name: "repo", value: repo),
|
|
.init(name: "x-error", value: "\(callbackBase)?action=error"),
|
|
.init(name: "command", value: "commit"),
|
|
.init(name: "message", value: message),
|
|
.init(name: "limit", value: "999"),
|
|
]
|
|
if !path.isEmpty { items.append(.init(name: "path", value: path)) }
|
|
items += [
|
|
.init(name: "command", value: "push"),
|
|
.init(name: "x-success", value: "\(callbackBase)?action=push"),
|
|
]
|
|
components.queryItems = items
|
|
if let url = components.url { UIApplication.shared.open(url) }
|
|
}
|
|
|
|
// MARK: - File operations
|
|
|
|
/// Write a file into the Working Copy repo (called by FileSyncPipeline on every save).
|
|
/// mode=overwrite: replaces even if the file has uncommitted changes.
|
|
/// askCommit=false: silent write, user commits manually from the Git panel.
|
|
func writeFile(repo: String, path: String, content: String, askCommit: Bool = false) {
|
|
var p: [String: String] = ["repo": repo, "path": path, "text": content, "mode": "overwrite"]
|
|
if askCommit { p["askcommit"] = "1" }
|
|
open(build("write", p, success: "write"))
|
|
}
|
|
|
|
// MARK: - Open in Working Copy
|
|
|
|
func openInWorkingCopy(repo: String, path: String, mode: String = "content") {
|
|
var c = URLComponents(string: "working-copy://open")!
|
|
c.queryItems = [.init(name: "repo", value: repo),
|
|
.init(name: "path", value: path),
|
|
.init(name: "mode", value: mode)]
|
|
if let url = c.url { UIApplication.shared.open(url) }
|
|
}
|
|
|
|
// MARK: - URL Builder
|
|
|
|
private func build(
|
|
_ command: String,
|
|
_ params: [String: String],
|
|
success action: String? = nil
|
|
) -> URL {
|
|
var c = URLComponents(string: "working-copy://x-callback-url/\(command)")!
|
|
var items: [URLQueryItem] = [
|
|
.init(name: "key", value: apiKey),
|
|
.init(name: "x-error", value: "\(callbackBase)?action=error"),
|
|
]
|
|
if let action {
|
|
// Append a trailing "&" so Working Copy appends the JSON result as a bare value
|
|
items.append(.init(name: "x-success", value: "\(callbackBase)?action=\(action)&"))
|
|
}
|
|
for (k, v) in params { items.append(.init(name: k, value: v)) }
|
|
c.queryItems = items
|
|
// URL construction from URLComponents with a valid scheme cannot fail,
|
|
// but guard defensively rather than force-unwrap.
|
|
guard let url = c.url else {
|
|
assertionFailure("Failed to build Working Copy URL for command: \(command)")
|
|
return URL(string: "working-copy://")!
|
|
}
|
|
return url
|
|
}
|
|
|
|
private func open(_ url: URL) {
|
|
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
|
}
|
|
}
|