gitbay/Views/Repos/CommitView.swift
136 lines · 5654 bytes
1import SwiftUI
2
3/// One commit: what it says, who signed it, whether it passed, and what
4/// it changed.
5struct CommitView: View {
6
7 @State private var model: CommitDetailViewModel
8
9 init(client: GitbayClient, repo: String, sha: String) {
10 _model = State(initialValue: CommitDetailViewModel(
11 client: client, repoPath: repo, sha: sha
12 ))
13 }
14
15 var body: some View {
16 List {
17 if let commit = model.state.value {
18 Section {
19 VStack(alignment: .leading, spacing: 6) {
20 Text(commit.subject)
21 .font(.gbSans(.headline))
22 if let message = commit.message,
23 !message.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
24 Text(message.trimmingCharacters(in: .whitespacesAndNewlines))
25 .font(.gbSans(.subheadline))
26 .foregroundStyle(.secondary)
27 }
28 HStack(spacing: 6) {
29 Text(commit.shortSHA)
30 .font(.gbMono(.caption))
31 .foregroundStyle(.secondary)
32 SignatureBadge(signature: commit.signature)
33 }
34 HStack(spacing: 6) {
35 Text(commit.authorName)
36 Text(commit.date, format: .dateTime.year().month().day())
37 .foregroundStyle(.tertiary)
38 }
39 .font(.gbSans(.caption))
40 .foregroundStyle(.secondary)
41 }
42 .padding(.vertical, 2)
43 }
44
45 if let checks = commit.checks, !checks.isEmpty {
46 Section("Checks") {
47 ForEach(checks) { check in
48 HStack {
49 Image(systemName: check.state == "success"
50 ? "checkmark.circle.fill"
51 : check.state == "pending" ? "circle.dotted" : "xmark.circle.fill")
52 .foregroundStyle(check.state == "success"
53 ? Color.gbOK
54 : check.state == "pending" ? Color.gbWarn : Color.gbBad)
55 Text(check.context)
56 .font(.gbSans(.subheadline))
57 Spacer()
58 Text(check.state)
59 .font(.gbSans(.caption))
60 .foregroundStyle(.secondary)
61 }
62 }
63 }
64 }
65
66 if let diff = model.diff, !diff.files.isEmpty {
67 Section("Changes +\(diff.additions) −\(diff.deletions)") {
68 ForEach(diff.files) { file in
69 NavigationLink(value: RepoRoute.file(
70 repo: model.repoPath, path: file.displayPath, ref: commit.sha
71 )) {
72 HStack {
73 Text(file.displayPath)
74 .font(.gbMono(.caption))
75 .lineLimit(1)
76 .truncationMode(.head)
77 Spacer()
78 Text("+\(file.additions)").foregroundStyle(Color.gbOK)
79 Text("−\(file.deletions)").foregroundStyle(Color.gbBad)
80 }
81 .font(.gbSans(.caption2))
82 }
83 }
84 }
85 }
86 }
87 }
88 .overlay { LoadStateOverlay(state: model.state) }
89 .navigationTitle(String(model.sha.prefix(10)))
90 .navigationBarTitleDisplayMode(.inline)
91 .toolbar {
92 ToolbarItem(placement: .topBarTrailing) {
93 if model.diff?.files.isEmpty == false {
94 NavigationLink(value: RepoRoute.commitDiff(
95 repo: model.repoPath, sha: model.sha
96 )) {
97 Image(systemName: "plus.forwardslash.minus")
98 }
99 .accessibilityIdentifier("commit-diff-button")
100 }
101 }
102 }
103 .task { await model.load() }
104 .refreshable { await model.load() }
105 }
106}
107
108/// The commit's patch, rendered by the same view the MR diff uses.
109struct CommitDiffView: View {
110
111 @State private var model: CommitDetailViewModel
112
113 init(client: GitbayClient, repo: String, sha: String) {
114 _model = State(initialValue: CommitDetailViewModel(
115 client: client, repoPath: repo, sha: sha
116 ))
117 }
118
119 var body: some View {
120 ZStack {
121 Color.clear
122 if let diff = model.diff, !diff.files.isEmpty {
123 List {
124 ForEach(diff.files) { file in
125 DiffFileSection(file: file)
126 }
127 }
128 .listStyle(.plain)
129 }
130 }
131 .overlay { LoadStateOverlay(state: model.state) }
132 .navigationTitle("Diff")
133 .navigationBarTitleDisplayMode(.inline)
134 .task { await model.load() }
135 }
136}