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 { private let apiKey: String private let callbackBase = "padxcode://git-callback" init(apiKey: String) { self.apiKey = apiKey } // 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 return c.url! } private func open(_ url: URL) { UIApplication.shared.open(url, options: [:], completionHandler: nil) } }