import Foundation import Observation /// One directory of `repo tree`. Navigation pushes a new instance per /// level, so back is free and each level revalidates independently. @Observable @MainActor final class TreeViewModel { private(set) var state: LoadState = .loading private let client: GitbayClient let repoPath: String let directory: String let ref: String? init(client: GitbayClient, repoPath: String, directory: String = "", ref: String? = nil) { self.client = client self.repoPath = repoPath self.directory = directory self.ref = ref } /// Directories first, then files, both alphabetically — the ordering /// every forge uses. var entries: [TreeEntry] { guard let listing = state.value else { return [] } return listing.entries.sorted { if $0.isDirectory != $1.isDirectory { return $0.isDirectory } return $0.name.localizedStandardCompare($1.name) == .orderedAscending } } func load() async { var argv = ["repo", "tree", repoPath] if !directory.isEmpty { argv.append(directory) } if let ref { argv.append(contentsOf: ["--ref", ref]) } do { state = .loaded(try await client.read(argv, as: TreeListing.self)) } catch { state = .from(error) } } func childDirectory(_ entry: TreeEntry) -> String { directory.isEmpty ? entry.name : "\(directory)/\(entry.name)" } }