gitbay/Views/Repos/CompareView.swift

v1.4.0
gitbay-ios/gitbay/Views/Repos/CompareView.swift history · blame · raw

134 lines · 4637 bytes

  1import SwiftUI
  2
  3/// Two refs, one diff — `repo diff`'s three-dot comparison, rendered the
  4/// same way a commit's patch is. `base` and `head` are picked from
  5/// `repo refs`; the request only fires when the user taps Compare.
  6struct CompareView: View {
  7
  8    @State private var model: CompareViewModel
  9
 10    init(client: GitbayClient, repo: String, defaultBranch: String? = nil) {
 11        let model = CompareViewModel(client: client, repoPath: repo)
 12        if let defaultBranch {
 13            model.base = defaultBranch
 14        }
 15        _model = State(initialValue: model)
 16    }
 17
 18    private var isComparing: Bool {
 19        if case .loading = model.state { return true }
 20        return false
 21    }
 22
 23    var body: some View {
 24        List {
 25            refsSection
 26
 27            Section {
 28                Button("Compare") {
 29                    Task { await model.compare() }
 30                }
 31                .disabled(isComparing
 32                          || model.base.trimmingCharacters(in: .whitespaces).isEmpty
 33                          || model.head.trimmingCharacters(in: .whitespaces).isEmpty)
 34                .accessibilityIdentifier("compare-submit")
 35            }
 36
 37            if model.truncated {
 38                Section {
 39                    GBNotice("The patch was too large and was cut short.", .gbWarn)
 40                }
 41            }
 42
 43            if let mergeBase = model.mergeBase {
 44                Section {
 45                    HStack {
 46                        Text("Merge base")
 47                        Spacer()
 48                        Text(String(mergeBase.prefix(10)))
 49                            .font(.gbMono(.caption))
 50                            .foregroundStyle(.secondary)
 51                    }
 52                }
 53            }
 54
 55            // `.loading` is shown by the overlay below; `.empty` and
 56            // `.failed` render inline so the pickers above stay reachable
 57            // instead of being covered by a frame-filling overlay.
 58            switch model.state {
 59            case .empty(let message):
 60                Section {
 61                    GBNotice(message, .gbWarn)
 62                }
 63            case .failed(let message):
 64                Section {
 65                    GBNotice(message, .gbWarn)
 66                }
 67            case .loading, .loaded:
 68                EmptyView()
 69            }
 70
 71            if let diff = model.state.value, !diff.files.isEmpty {
 72                ForEach(diff.files) { file in
 73                    DiffFileSection(file: file)
 74                }
 75            }
 76        }
 77        .overlay {
 78            if isComparing {
 79                LoadStateOverlay(state: model.state)
 80            }
 81        }
 82        .navigationTitle("Compare")
 83        .navigationBarTitleDisplayMode(.inline)
 84        .task { await model.loadRefs() }
 85    }
 86
 87    @ViewBuilder
 88    private var refsSection: some View {
 89        Section {
 90            if let refs = model.refs {
 91                if refs.branches.isEmpty && refs.tags.isEmpty {
 92                    GBNotice("This repository has no branches or tags to compare.", .gbWarn)
 93                } else {
 94                    refPicker("Base", refs: refs, selection: Bindable(model).base)
 95                    refPicker("Head", refs: refs, selection: Bindable(model).head)
 96                }
 97            } else if let refsError = model.refsError {
 98                GBNotice(refsError, .gbWarn)
 99            } else {
100                ProgressView()
101            }
102        }
103    }
104
105    private func refPicker(_ title: String, refs: RepoRefs, selection: Binding<String>) -> some View {
106        Menu {
107            if !refs.branches.isEmpty {
108                Section("Branches") {
109                    ForEach(refs.branches) { ref in
110                        Button(ref.name) { selection.wrappedValue = ref.name }
111                    }
112                }
113            }
114            if !refs.tags.isEmpty {
115                Section("Tags") {
116                    ForEach(refs.tags) { ref in
117                        Button(ref.name) { selection.wrappedValue = ref.name }
118                    }
119                }
120            }
121        } label: {
122            HStack {
123                Text(title)
124                Spacer()
125                Text(selection.wrappedValue.isEmpty ? "Select a ref" : selection.wrappedValue)
126                    .foregroundStyle(selection.wrappedValue.isEmpty ? .secondary : .primary)
127                Image(systemName: "chevron.up.chevron.down")
128                    .font(.gbSans(.caption2))
129                    .foregroundStyle(.secondary)
130            }
131        }
132        .accessibilityIdentifier("compare-\(title.lowercased())-menu")
133    }
134}