| @@ -0,0 +1,125 @@ |
| 1 | import 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. |
| 6 | struct CompareView: View { |
| 7 | |
| 8 | @State private var model: CompareViewModel |
| 9 | /// `model.state` defaults to `.loading` before any comparison is |
| 10 | /// requested, since `compare()` is manually triggered rather than |
| 11 | /// run from `.task`. This tracks whether that default is a real |
| 12 | /// request in flight, so the overlay doesn't spin before the user |
| 13 | /// has picked anything. |
| 14 | @State private var hasCompared = false |
| 15 | |
| 16 | init(client: GitbayClient, repo: String, defaultBranch: String? = nil) { |
| 17 | let model = CompareViewModel(client: client, repoPath: repo) |
| 18 | if let defaultBranch { |
| 19 | model.base = defaultBranch |
| 20 | } |
| 21 | _model = State(initialValue: model) |
| 22 | } |
| 23 | |
| 24 | private var isComparing: Bool { |
| 25 | guard hasCompared, case .loading = model.state else { return false } |
| 26 | return true |
| 27 | } |
| 28 | |
| 29 | var body: some View { |
| 30 | List { |
| 31 | refsSection |
| 32 | |
| 33 | Section { |
| 34 | Button("Compare") { |
| 35 | hasCompared = true |
| 36 | Task { await model.compare() } |
| 37 | } |
| 38 | .disabled(isComparing |
| 39 | || model.base.trimmingCharacters(in: .whitespaces).isEmpty |
| 40 | || model.head.trimmingCharacters(in: .whitespaces).isEmpty) |
| 41 | .accessibilityIdentifier("compare-submit") |
| 42 | } |
| 43 | |
| 44 | if model.truncated { |
| 45 | Section { |
| 46 | GBNotice("The patch was too large and was cut short.", .gbWarn) |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | if let mergeBase = model.mergeBase { |
| 51 | Section { |
| 52 | HStack { |
| 53 | Text("Merge base") |
| 54 | Spacer() |
| 55 | Text(String(mergeBase.prefix(10))) |
| 56 | .font(.gbMono(.caption)) |
| 57 | .foregroundStyle(.secondary) |
| 58 | } |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | if let diff = model.state.value, !diff.files.isEmpty { |
| 63 | ForEach(diff.files) { file in |
| 64 | DiffFileSection(file: file) |
| 65 | } |
| 66 | } |
| 67 | } |
| 68 | .overlay { |
| 69 | if hasCompared { |
| 70 | LoadStateOverlay(state: model.state) |
| 71 | } |
| 72 | } |
| 73 | .navigationTitle("Compare") |
| 74 | .navigationBarTitleDisplayMode(.inline) |
| 75 | .task { await model.loadRefs() } |
| 76 | } |
| 77 | |
| 78 | @ViewBuilder |
| 79 | private var refsSection: some View { |
| 80 | Section { |
| 81 | if let refs = model.refs { |
| 82 | if refs.branches.isEmpty && refs.tags.isEmpty { |
| 83 | GBNotice("This repository has no branches or tags to compare.", .gbWarn) |
| 84 | } else { |
| 85 | refPicker("Base", refs: refs, selection: Bindable(model).base) |
| 86 | refPicker("Head", refs: refs, selection: Bindable(model).head) |
| 87 | } |
| 88 | } else if let refsError = model.refsError { |
| 89 | GBNotice(refsError, .gbWarn) |
| 90 | } else { |
| 91 | ProgressView() |
| 92 | } |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | private func refPicker(_ title: String, refs: RepoRefs, selection: Binding<String>) -> some View { |
| 97 | Menu { |
| 98 | if !refs.branches.isEmpty { |
| 99 | Section("Branches") { |
| 100 | ForEach(refs.branches) { ref in |
| 101 | Button(ref.name) { selection.wrappedValue = ref.name } |
| 102 | } |
| 103 | } |
| 104 | } |
| 105 | if !refs.tags.isEmpty { |
| 106 | Section("Tags") { |
| 107 | ForEach(refs.tags) { ref in |
| 108 | Button(ref.name) { selection.wrappedValue = ref.name } |
| 109 | } |
| 110 | } |
| 111 | } |
| 112 | } label: { |
| 113 | HStack { |
| 114 | Text(title) |
| 115 | Spacer() |
| 116 | Text(selection.wrappedValue.isEmpty ? "Select a ref" : selection.wrappedValue) |
| 117 | .foregroundStyle(selection.wrappedValue.isEmpty ? .secondary : .primary) |
| 118 | Image(systemName: "chevron.up.chevron.down") |
| 119 | .font(.gbSans(.caption2)) |
| 120 | .foregroundStyle(.secondary) |
| 121 | } |
| 122 | } |
| 123 | .accessibilityIdentifier("compare-\(title.lowercased())-menu") |
| 124 | } |
| 125 | } |