~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
229 lines · 9.4 KB Raw History
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)
        }
    }
}