gitbay/Repos/FileViewModel.swift
71 lines · 2396 bytes
1import Foundation
2import Observation
3
4/// One file via `repo cat`: text renders (highlighted when the language is
5/// known), binary says so, truncated says so — never a guess.
6@Observable
7@MainActor
8final class FileViewModel {
9
10 private(set) var state: LoadState<FileContent> = .loading
11 private(set) var actionError: String?
12 private(set) var working = false
13
14 private let client: GitbayClient
15 let repoPath: String
16 let filePath: String
17 let ref: String?
18
19 init(client: GitbayClient, repoPath: String, filePath: String, ref: String? = nil) {
20 self.client = client
21 self.repoPath = repoPath
22 self.filePath = filePath
23 self.ref = ref
24 }
25
26 var fileName: String {
27 String(filePath.split(separator: "/").last ?? "")
28 }
29
30 func load() async {
31 var argv = ["repo", "cat", repoPath, filePath]
32 if let ref { argv.append(contentsOf: ["--ref", ref]) }
33 do {
34 state = .loaded(try await client.read(argv, as: FileContent.self))
35 } catch {
36 state = .from(error)
37 }
38 }
39
40 /// The branch an edit commits to. `repo cat` echoes the ref it read,
41 /// so editing writes back to whatever is on screen.
42 var editableBranch: String? {
43 guard let file = state.value, !file.binary, !file.isTruncated else { return nil }
44 return file.ref
45 }
46
47 /// `repo commit-file` — the server refuses when the repository wants
48 /// signed commits, when the account has no verified email, or when
49 /// the repo is archived, and says which.
50 func commit(content: String, message: String) async -> Bool {
51 guard let branch = editableBranch else { return false }
52 working = true
53 actionError = nil
54 defer { working = false }
55 var argv = ["repo", "commit-file", repoPath, filePath, "--ref", branch, "--file", "-"]
56 let trimmed = message.trimmingCharacters(in: .whitespacesAndNewlines)
57 if !trimmed.isEmpty {
58 argv.append(contentsOf: ["--message", trimmed])
59 }
60 do {
61 try await client.run(argv, stdin: content)
62 await load()
63 return true
64 } catch let error as GitbayError {
65 actionError = error.userFacingMessage
66 } catch {
67 actionError = GitbayError.transport(error).userFacingMessage
68 }
69 return false
70 }
71}