Commit c6150bdc21

c6150bdc21900d0d43355c037ecf54c441d1907c

parent: d80ba74bc1

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-06 22:05 UTC

Compare screen with two ref pickers

Layout: unified · split

gitbay/ContentView.swift +2
@@ -96,6 +96,8 @@ private struct RouteDestinations: ViewModifier {
9696 BlameView(client: client, repo: repo, path: path, ref: ref)
9797 case .refs(let repo):
9898 RefsView(client: client, repo: repo)
99 case .compare(let repo, let defaultBranch):
100 CompareView(client: client, repo: repo, defaultBranch: defaultBranch)
99101 case .milestones(let repo):
100102 MilestoneListView(client: client, repo: repo)
101103 case .labels(let repo):
gitbay/Views/Repos/CompareView.swift added +125
@@ -0,0 +1,125 @@
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 /// `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}
gitbay/Views/Repos/RefsView.swift +7
@@ -11,6 +11,13 @@ struct RefsView: View {
1111
1212 var body: some View {
1313 List {
14 Section {
15 NavigationLink(value: RepoRoute.compare(
16 repo: model.repoPath, defaultBranch: model.defaultBranch
17 )) {
18 Label("Compare", systemImage: "arrow.left.arrow.right")
19 }
20 }
1421 if !model.branches.isEmpty {
1522 Section("Branches") {
1623 ForEach(model.branches) { ref in
gitbay/Views/Repos/RepoRoute.swift +1
@@ -11,6 +11,7 @@ nonisolated enum RepoRoute: Hashable {
1111 case grep(repo: String)
1212 case blame(repo: String, path: String, ref: String?)
1313 case refs(repo: String)
14 case compare(repo: String, defaultBranch: String?)
1415 case milestones(repo: String)
1516 case labels(repo: String)
1617 case wiki(repo: String)