import Foundation import Observation /// One file via `repo cat`: text renders (highlighted when the language is /// known), binary says so, truncated says so — never a guess. @Observable @MainActor final class FileViewModel { private(set) var state: LoadState = .loading private(set) var actionError: String? private(set) var working = false private let client: GitbayClient let repoPath: String let filePath: String let ref: String? init(client: GitbayClient, repoPath: String, filePath: String, ref: String? = nil) { self.client = client self.repoPath = repoPath self.filePath = filePath self.ref = ref } var fileName: String { String(filePath.split(separator: "/").last ?? "") } func load() async { var argv = ["repo", "cat", repoPath, filePath] if let ref { argv.append(contentsOf: ["--ref", ref]) } do { state = .loaded(try await client.read(argv, as: FileContent.self)) } catch { state = .from(error) } } /// The branch an edit commits to. `repo cat` echoes the ref it read, /// so editing writes back to whatever is on screen. var editableBranch: String? { guard let file = state.value, !file.binary, !file.isTruncated else { return nil } return file.ref } /// `repo commit-file` — the server refuses when the repository wants /// signed commits, when the account has no verified email, or when /// the repo is archived, and says which. func commit(content: String, message: String) async -> Bool { guard let branch = editableBranch else { return false } working = true actionError = nil defer { working = false } var argv = ["repo", "commit-file", repoPath, filePath, "--ref", branch, "--file", "-"] let trimmed = message.trimmingCharacters(in: .whitespacesAndNewlines) if !trimmed.isEmpty { argv.append(contentsOf: ["--message", trimmed]) } do { try await client.run(argv, stdin: content) await load() return true } catch let error as GitbayError { actionError = error.userFacingMessage } catch { actionError = GitbayError.transport(error).userFacingMessage } return false } }