import SwiftUI /// Two refs, one diff — `repo diff`'s three-dot comparison, rendered the /// same way a commit's patch is. `base` and `head` are picked from /// `repo refs`; the request only fires when the user taps Compare. struct CompareView: View { @State private var model: CompareViewModel init(client: GitbayClient, repo: String, defaultBranch: String? = nil) { let model = CompareViewModel(client: client, repoPath: repo) if let defaultBranch { model.base = defaultBranch } _model = State(initialValue: model) } private var isComparing: Bool { if case .loading = model.state { return true } return false } var body: some View { List { refsSection Section { Button("Compare") { Task { await model.compare() } } .disabled(isComparing || model.base.trimmingCharacters(in: .whitespaces).isEmpty || model.head.trimmingCharacters(in: .whitespaces).isEmpty) .accessibilityIdentifier("compare-submit") } if model.truncated { Section { GBNotice("The patch was too large and was cut short.", .gbWarn) } } if let mergeBase = model.mergeBase { Section { HStack { Text("Merge base") Spacer() Text(String(mergeBase.prefix(10))) .font(.gbMono(.caption)) .foregroundStyle(.secondary) } } } // `.loading` is shown by the overlay below; `.empty` and // `.failed` render inline so the pickers above stay reachable // instead of being covered by a frame-filling overlay. switch model.state { case .empty(let message): Section { GBNotice(message, .gbWarn) } case .failed(let message): Section { GBNotice(message, .gbWarn) } case .loading, .loaded: EmptyView() } if let diff = model.state.value, !diff.files.isEmpty { ForEach(diff.files) { file in DiffFileSection(file: file) } } } .overlay { if isComparing { LoadStateOverlay(state: model.state) } } .navigationTitle("Compare") .navigationBarTitleDisplayMode(.inline) .task { await model.loadRefs() } } @ViewBuilder private var refsSection: some View { Section { if let refs = model.refs { if refs.branches.isEmpty && refs.tags.isEmpty { GBNotice("This repository has no branches or tags to compare.", .gbWarn) } else { refPicker("Base", refs: refs, selection: Bindable(model).base) refPicker("Head", refs: refs, selection: Bindable(model).head) } } else if let refsError = model.refsError { GBNotice(refsError, .gbWarn) } else { ProgressView() } } } private func refPicker(_ title: String, refs: RepoRefs, selection: Binding) -> some View { Menu { if !refs.branches.isEmpty { Section("Branches") { ForEach(refs.branches) { ref in Button(ref.name) { selection.wrappedValue = ref.name } } } } if !refs.tags.isEmpty { Section("Tags") { ForEach(refs.tags) { ref in Button(ref.name) { selection.wrappedValue = ref.name } } } } } label: { HStack { Text(title) Spacer() Text(selection.wrappedValue.isEmpty ? "Select a ref" : selection.wrappedValue) .foregroundStyle(selection.wrappedValue.isEmpty ? .secondary : .primary) Image(systemName: "chevron.up.chevron.down") .font(.gbSans(.caption2)) .foregroundStyle(.secondary) } } .accessibilityIdentifier("compare-\(title.lowercased())-menu") } }