gitbay/Views/Repos/CommitView.swift

1c5622210d0f72efd86582ce195a6741401e7939
gitbay-ios/gitbay/Views/Repos/CommitView.swift history · blame · raw

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}