import SwiftUI /// A commit in a list: its subject, the start of the rest, and who made it. struct CommitRow: View { let repo: RepoRef let commit: Commit var body: some View { VStack(alignment: .leading, spacing: Space.s1_5) { HStack(alignment: .firstTextBaseline, spacing: Space.s2_5) { Text(commit.subject).font(.body.weight(.semibold)).lineLimit(1) Spacer(minLength: 0) Text(When.short(commit.when)).font(.subheadline).foregroundStyle(.secondary).fixedSize() } if !commit.details.isEmpty { Text(commit.details.replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression)) .font(.footnote.monospaced()) .foregroundStyle(.secondary) .lineLimit(1) } CommitPeople(commit: commit, whole: false) } .background { // Behind the row, so that the list draws no chevron for it. NavigationLink(value: Route.commit(repo, sha: commit.sha)) { EmptyView() }.opacity(0) } } } /// Their faces, and who wrote it. A list stops there: a queued pull lands as /// the squash its runner commits, so saying who committed each would name that /// runner on every row. The commit's own page says, when `whole`. private struct CommitPeople: View { let commit: Commit let whole: Bool private func names(_ people: [CommitAuthor]) -> Text { var out = Text("") for (i, person) in people.enumerated() { if i > 0 { let joiner = i == people.count - 1 ? " and " : ", " out = Text("\(out)\(Text(joiner).foregroundStyle(.secondary))") } out = Text("\(out)\(Text(person.label).fontWeight(.semibold))") } return out } var body: some View { let authors = commit.authors HStack(spacing: Space.s2) { HStack(spacing: -6) { ForEach(Array(authors.prefix(2).enumerated()), id: \.offset) { _, person in Avatar(url: person.actor?.avatar, name: person.label, did: person.actor?.did, face: person.face, size: Size.Avatar.small) } } if whole, let committer = commit.otherCommitter { Text("\(names(authors))\(Text(" authored and ").foregroundStyle(.secondary))\(names([committer]))\(Text(" committed").foregroundStyle(.secondary))") } else { Text("\(names(authors))\(Text(" committed").foregroundStyle(.secondary))") } } .font(.subheadline) } } struct CommitView: View { let repo: RepoRef let sha: String private enum Tab: Hashable { case changes, details } @State private var tab = Tab.changes var body: some View { Loading(load: { [repo, sha] in let answer: CommitAnswer = try await Client.shared.query("getCommit", ["repo": repo.uri, "sha": sha]) return answer }) { answer in switch tab { case .changes: CommitChanges(repo: repo, answer: answer) case .details: CommitDetails(repo: repo, commit: answer.commit) } } .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .principal) { Picker("Show", selection: $tab) { Text("Changes").tag(Tab.changes) Text("Details").tag(Tab.details) } .pickerStyle(.segmented) .fixedSize() } if let url = URL(string: "https://knotless.dev/\(repo.handle)/\(repo.name)/commit/\(sha)") { ToolbarItem(placement: .topBarTrailing) { ShareLink(item: url) } } } } } /// What it says, then what it changed, file by file. private struct CommitChanges: View { let repo: RepoRef let answer: CommitAnswer private var commit: Commit { answer.commit } var body: some View { ScrollViewReader { reader in ScrollView { VStack(alignment: .leading, spacing: Space.s3) { HStack(alignment: .firstTextBaseline, spacing: Space.s2_5) { CommitPeople(commit: commit, whole: true) Spacer(minLength: 0) Text(When.short(commit.when)).font(.subheadline).foregroundStyle(.secondary).fixedSize() } Text(commit.subject).font(.headline) if !commit.details.isEmpty { MarkdownView(source: commit.details, place: MarkdownPlace(repo: repo)) } Text(summary).font(.footnote).foregroundStyle(.secondary) } .padding(Space.s4) .frame(maxWidth: .infinity, alignment: .leading) LazyVStack(alignment: .leading, spacing: 0, pinnedViews: [.sectionHeaders]) { ForEach(answer.files) { file in FileSection(repo: repo, file: file).id(file.id) } } .padding(.bottom, Size.floatingClearance) } .overlay(alignment: .bottomTrailing) { if answer.files.count > 1 { Menu { ForEach(answer.files) { file in Button(file.path) { withAnimation { reader.scrollTo(file.id, anchor: .top) } } } } label: { SwiftUI.Label { Text("Jump to file") } icon: { Oct(name: "file-moved") }.labelStyle(TightLabel()) } .floating() .padding(Space.s4) } } } } private var summary: String { let files = answer.files.count == 1 ? "1 file changed" : "\(answer.files.count) files changed" let added = answer.files.reduce(0) { $0 + $1.added } let removed = answer.files.reduce(0) { $0 + $1.removed } return "\(files), +\(added) -\(removed)" } } /// Its name, who made it, what it follows and the pull that brought it in. private struct CommitDetails: View { let repo: RepoRef let commit: Commit @State private var copied = false var body: some View { List { Group { Section("Commit") { HStack(spacing: Space.s2_5) { Text(commit.sha).font(.body.monospaced()).lineLimit(1).truncationMode(.tail) Spacer(minLength: 0) Button { UIPasteboard.general.string = commit.sha copied = true } label: { Oct(name: copied ? "check" : "copy", size: Icon.xl).foregroundStyle(copied ? Color.success : Color.accent) } .buttonStyle(.borderless) .accessibilityLabel(copied ? "Copied" : "Copy") } } Section("Contributors") { ForEach(Array(contributors.enumerated()), id: \.offset) { _, one in if let handle = one.person.actor?.handle { NavigationLink(value: Route.profile(handle)) { person(one.person, one.role) } } else { person(one.person, one.role) } } } if let parents = commit.parents, !parents.isEmpty { Section(parents.count == 1 ? "Parent" : "Parents") { ForEach(parents, id: \.sha) { parent in NavigationLink(value: Route.commit(repo, sha: parent.sha)) { HStack(spacing: Space.s2_5) { Oct(name: "git-commit", size: Icon.xl).foregroundStyle(.secondary) Text(parent.sha).font(.body.monospaced()).lineLimit(1).truncationMode(.tail) } } } } } if let pull = commit.pull { Section("Pull") { NavigationLink(value: Route.thread(repo, uri: pull.uri)) { HStack(spacing: Space.s2_5) { Oct(name: "git-pull-request", size: Icon.xl).foregroundStyle(.secondary) Text(pull.title ?? "This commit's pull").lineLimit(2) } } } } } .listRowBackground(Color.card) } } private var contributors: [(person: CommitAuthor, role: String)] { var out = (commit.author.map { [(person: $0, role: "Author")] } ?? []) out += (commit.coauthors ?? []).map { (person: $0, role: "Co-author") } if let committer = commit.otherCommitter { out.append((person: committer, role: "Committer")) } return out } private func person(_ person: CommitAuthor, _ role: String) -> some View { HStack(spacing: Space.s3) { Avatar(url: person.actor?.avatar, name: person.label, did: person.actor?.did, face: person.face, size: Size.Avatar.medium) Text(person.label).lineLimit(1) Spacer(minLength: 8) Text(role).foregroundStyle(.secondary) } } }