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

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
888 lines · 25.4 KB Raw History
import Foundation

struct Person: Decodable, Hashable, Identifiable {
    let did: String
    let handle: String
    var avatar: String?
    var id: String { did }
}

/// A page of accounts: who follows someone, or whom they follow.
struct ActorPage: Decodable, Sendable {
    let actors: [Person]
    var cursor: String?
}

/// A repository as the feed and notifications name it.
struct RepoRef: Decodable, Hashable {
    let did: String
    let handle: String
    let name: String
    var uri: String { "at://\(did)/dev.knotless.repo/\(name)" }
    var label: String { "\(handle)/\(name)" }
}

struct Repo: Decodable, Identifiable, Hashable {
    let uri: String
    let owner: Person
    let name: String
    var description: String?
    var starCount: Int?
    var forkCount: Int?
    var `private`: Bool?
    var language: Language?
    var id: String { uri }
    var ref: RepoRef { RepoRef(did: owner.did, handle: owner.handle, name: name) }
}

struct Language: Decodable, Hashable {
    let name: String
    var color: String?
}

/// A link a repository or an account shows, by its title or else its host.
struct WebLink: Decodable, Hashable {
    let url: String
    var title: String?
    var label: String { title ?? URL(string: url)?.host() ?? url }
}

struct RepoList: Decodable {
    var repos: [Repo] = []
}

struct RepoDetail: Decodable {
    let uri: String
    let owner: Person
    let name: String
    var description: String?
    let defaultBranch: String
    let openPulls: Int
    let openIssues: Int
    var starCount: Int?
    var `private`: Bool?
    var links: [WebLink]?
    var forkOf: Basic?
    var releases: Releases?
    var viewer: Viewing?

    struct Viewing: Decodable {
        /// The asker's star of it, if they starred it.
        var star: String?
        /// What they chose to hear of it: `all`, `releases` or `ignore`.
        var subscription: String?
    }

    struct Basic: Decodable, Hashable {
        let did: String
        let handle: String
        let name: String
        var ref: RepoRef { RepoRef(did: did, handle: handle, name: name) }
    }

    struct Releases: Decodable {
        let count: Int
        var release: Latest?
        struct Latest: Decodable {
            let package: String
            let version: String
            var latest: Bool?
            var createdAt: String?
        }
    }
}

struct RepoMade: Decodable, Sendable {
    let uri: String
    let name: String
}

struct Branch: Decodable, Identifiable, Hashable {
    let name: String
    var id: String { name }
}

struct BranchList: Decodable {
    var branches: [Branch] = []
}

struct CommitList: Decodable {
    var commits: [Commit] = []
    var cursor: String?
}

struct Release: Decodable, Identifiable, Hashable {
    let uri: String
    let package: String
    let version: String
    var latest: Bool?
    var notes: String?
    var html: String?
    var files: [File]?
    var createdAt: String?
    var id: String { uri }

    struct File: Decodable, Hashable {
        let name: String
        var size: Int?
    }
}

struct ReleaseList: Decodable {
    var releases: [Release] = []
    var cursor: String?
}

struct Run: Decodable, Identifiable, Hashable {
    let check: Check
    let ref: String
    var pull: Pull?
    var batch: Int?
    var subject: String?
    var id: String { check.id }

    struct Pull: Decodable, Hashable { var title: String? }
}

struct RunList: Decodable {
    var runs: [Run] = []
    var cursor: String?
    var workflows: [Workflow] = []
}

/// A workflow on the default branch.
struct Workflow: Decodable, Hashable {
    let name: String
    /// It runs only when asked for by name.
    var asked: Bool?
    /// The first sentence of the comment its file opens with.
    var about: String?
}

/// One run, as its runner filed it: each step, and the end of what they printed.
struct RunDetail: Decodable, Hashable {
    let check: Check
    let ref: String
    var pull: String?
    let runner: Person
    var steps: [Step] = []
    /// Each step's after a line `==> <name>`.
    var log: String?
}

struct Step: Decodable, Hashable {
    let name: String
    /// `success`, `failure`, `skipped` or `cancelled`; `running` only while it runs.
    let status: String
    /// Absent when it was killed.
    var exitCode: Int?
    var durationMs: Int?
}

/// The workflows at a ref, and what runs each there.
struct WorkflowList: Decodable, Hashable {
    /// In full.
    let ref: String
    var workflows: [Entry] = []

    struct Entry: Decodable, Hashable {
        let name: String
        let push: Bool
        let pull: Bool
        /// Its owner may ask for it at the ref by name.
        let asked: Bool
    }
}

struct CheckImage: Decodable, Hashable {
    let alt: String
    /// On this server, from its root.
    let url: String
}

struct ItemLabel: Decodable, Hashable {
    let name: String
    var color: String?
}

struct Item: Decodable, Identifiable, Hashable {
    let uri: String
    let kind: String
    let title: String
    let state: String
    let author: Person
    let createdAt: String
    let comments: Int
    var draft: Bool?
    var labels: [ItemLabel]?
    var id: String { uri }
}

struct ItemList: Decodable {
    var items: [Item] = []
    var openCount: Int?
    var closedCount: Int?
    var cursor: String?
}

struct DiffRow: Decodable, Hashable {
    let kind: String
    let line: Bool
    var old: Int?
    var new: Int?
    let text: String
}

struct DiffFile: Decodable, Identifiable, Hashable {
    let old: String
    let new: String
    let status: String
    let added: Int
    let removed: Int
    var note: String?
    var ranges: String?
    var rows: [DiffRow] = []
    var rowCount: Int?
    var id: String { old + "\u{0}" + new }
    var path: String { new.isEmpty ? old : new }

    enum CodingKeys: String, CodingKey { case old, new, status, added, removed, note, ranges, rows, rowCount }
    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        old = try c.decode(String.self, forKey: .old)
        new = try c.decode(String.self, forKey: .new)
        status = try c.decode(String.self, forKey: .status)
        added = try c.decode(Int.self, forKey: .added)
        removed = try c.decode(Int.self, forKey: .removed)
        note = try c.decodeIfPresent(String.self, forKey: .note)
        ranges = try c.decodeIfPresent(String.self, forKey: .ranges)
        rows = try c.decodeIfPresent([DiffRow].self, forKey: .rows) ?? []
        rowCount = try c.decodeIfPresent(Int.self, forKey: .rowCount)
    }
}

struct CommitAuthor: Decodable, Hashable {
    var name: String?
    var actor: Person?
    /// The coding agent whose address it is.
    var agent: String?
    var label: String { actor?.handle ?? name ?? "" }
    /// The agent's own face, where the app has it.
    var face: String? { agent == "claude" ? "agent-claude" : nil }
}

struct Commit: Decodable, Identifiable, Hashable {
    let sha: String
    let message: String
    var author: CommitAuthor?
    var coauthors: [CommitAuthor]?
    var committer: CommitAuthor?
    var parents: [Parent]?
    var date: String?
    var committedAt: String?
    var pull: Pull?
    var id: String { sha }
    var subject: String { message.split(separator: "\n", maxSplits: 1).first.map(String.init) ?? message }
    /// What its message says after its subject.
    var details: String {
        message.split(separator: "\n", maxSplits: 1, omittingEmptySubsequences: false).dropFirst().first
            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } ?? ""
    }
    var when: String? { committedAt ?? date }
    var authors: [CommitAuthor] { (author.map { [$0] } ?? []) + (coauthors ?? []) }
    /// Who committed it, where that's someone other than those who wrote it.
    var otherCommitter: CommitAuthor? {
        guard let committer, !authors.contains(where: { $0.label == committer.label }) else { return nil }
        return committer
    }

    struct Parent: Decodable, Hashable { let sha: String }
    struct Pull: Decodable, Hashable {
        let uri: String
        var title: String?
    }
}

/// What was searched for, on this phone, newest first, as one string to keep.
enum RecentSearches {
    static let key = "recentSearches"
    private static let most = 10

    static func decode(_ text: String) -> [String] { text.split(separator: "\n").map(String.init) }

    static func adding(_ query: String, to text: String) -> String {
        let clean = query.replacing("\n", with: " ").trimmingCharacters(in: .whitespaces)
        guard !clean.isEmpty else { return text }
        return ([clean] + decode(text).filter { $0 != clean }).prefix(most).joined(separator: "\n")
    }
}

/// `listActorItems`: pulls or issues, each with the repository it's on.
struct ActorItems: Decodable, Sendable {
    let items: [ActorItem]
    var cursor: String?
}

struct ActorItem: Decodable, Identifiable, Hashable, Sendable {
    let item: Item
    let repo: RepoRef
    var id: String { item.uri }
}

/// `searchCode`.
struct CodeSearch: Decodable, Sendable {
    var files: [File]?
    var cursor: String?

    struct File: Decodable, Identifiable, Hashable, Sendable {
        let repo: RepoHit
        let path: String
        var language: String?
        var chunks: [Chunk]?
        var id: String { repo.did + "/" + repo.name + "/" + path }
    }

    struct Chunk: Decodable, Hashable, Sendable {
        let startLine: Int
        let content: String
        var ranges: [Match]?
        var highlights: String?

    }

    struct Match: Decodable, Hashable, Sendable {
        let line: Int
        let byteStart: Int
        let byteEnd: Int
    }
}

/// A repository in `listRepos` or `searchCode`, whose owner may not be named.
struct RepoHit: Decodable, Hashable, Sendable {
    let did: String
    let name: String
    var description: String?
    var owner: Person?
    var `private`: Bool?
    var starCount: Int?
    var language: Language?

    var ref: RepoRef { RepoRef(did: did, handle: owner?.handle ?? did, name: name) }
    var repo: Repo {
        Repo(
            uri: "at://\(did)/dev.knotless.repo/\(name)",
            owner: owner ?? Person(did: did, handle: did),
            name: name, description: description, starCount: starCount, private: `private`, language: language)
    }
}

struct RepoSearch: Decodable, Sendable {
    let repos: [RepoHit]
    var cursor: String?
}

struct OwnerSearch: Decodable, Sendable {
    let owners: [Found]

    struct Found: Decodable, Hashable, Sendable {
        let owner: Person
        let repoCount: Int
    }
}

struct CommitAnswer: Decodable {
    let commit: Commit
    var files: [DiffFile] = []
}

struct Changes: Decodable {
    /// Where its commits can be read, which for an open pull from a fork is the fork.
    var repo: RepoDetail.Basic?
    var commits: [Commit]?
    var files: [DiffFile]?
}

struct Comment: Decodable, Identifiable, Hashable {
    var uri: String?
    let author: Person
    let createdAt: String
    var body: String?
    var html: String?
    var verdict: String?
    /// The lines of a pull's diff it's on, if it's on any.
    var line: Lines?
    /// The comment starting the thread on those lines that this answers.
    var reply: String?
    var resolved: Bool?
    var reactions: [ReactionCount]?
    var id: String { uri ?? (author.did + createdAt) }

    struct Lines: Decodable, Hashable {
        let path: String
        var side: String?
        let line: Int
        var startLine: Int?
        let outdated: Bool
        /// As they were when it was written; absent when that can't be read.
        var lines: [Text]?

        struct Text: Decodable, Hashable {
            let number: Int
            let text: String
        }
    }

    /// Each thread on lines together: answers straight after what they answer,
    /// wherever they fell in time, as the pages show them.
    static func threaded(_ comments: [Comment]) -> [Comment] {
        let starts = Set(comments.compactMap(\.uri))
        var answers: [String: [Comment]] = [:]
        for comment in comments {
            if let root = comment.reply, starts.contains(root) { answers[root, default: []].append(comment) }
        }
        return comments.flatMap { comment -> [Comment] in
            if let root = comment.reply, starts.contains(root) { return [] }
            return [comment] + (comment.uri.flatMap { answers[$0] } ?? [])
        }
    }
}

struct Check: Decodable, Identifiable, Hashable {
    let uri: String
    let workflow: String
    let status: String
    /// Why it was skipped, when it was.
    var skipped: String?
    var startedAt: String?
    var finishedAt: String?
    var commit: String?
    /// Why it couldn't run.
    var error: String?
    var images: [CheckImage]?
    var id: String { uri }
}

struct Review: Decodable, Hashable {
    let reviewer: Person
    var verdict: String?
    var outdated: Bool?
}

struct PullHead: Decodable, Hashable {
    var branch: String?
}

struct PullThread: Decodable {
    let item: Item
    var body: String?
    var html: String?
    var base: String?
    var head: PullHead?
    var merged: String?
    var comments: [Comment]?
    var reviews: [Review]?
    var checks: [Check]?
    var changes: Changes?
    var reactions: [ReactionCount]?
    var viewer: ThreadViewer?
    var queue: QueueInfo?
}

struct ReactionCount: Decodable, Hashable {
    let content: String
    var count: Int
    /// The asker's own reaction of this content, if they reacted so.
    var reaction: String?
}

struct ThreadViewer: Decodable {
    var setState: Bool?
}

/// Where a pull stands in the merge queue, if it was queued or ejected.
struct QueueInfo: Decodable, Hashable {
    let state: String
    /// Where it stands, in the server's words.
    let said: String
    var reason: String?
}

/// Whether a pull can merge, and what the asker may do about it.
struct MergeStatus: Decodable {
    let state: String
    var checks: String?
    var queueOnly: Bool?
    var queueBlocked: String?
    var reviews: String?
    var reviewsMissing: [Note]?
    var may: May?

    struct Note: Decodable {
        let text: String
    }

    struct May: Decodable {
        var merge: Bool?
        var queue: Bool?
    }
}

struct DiffAnswer: Decodable {
    var files: [DiffFile] = []
}

struct TreeEntry: Decodable, Identifiable, Hashable {
    let name: String
    let kind: String
    var size: Int?
    var id: String { name }
}

struct Tree: Decodable {
    var ref: String?
    var path: String?
    var lastCommit: Commit?
    var entries: [TreeEntry]?
    var readme: Readme?
    var checks: [Check]?
    struct Readme: Decodable { var name: String? }
}

struct FileAnswer: Decodable {
    var ref: String?
    var path: String?
    var kind: String?
    var size: Int?
    var lines: Int?
    var text: String?
    var ranges: String?
    /// A Markdown file, made into HTML.
    var html: String?
}

struct Org: Decodable, Hashable { let actor: Person }

struct Profile: Decodable {
    let did: String
    let handle: String
    var displayName: String?
    var description: String?
    var pronouns: String?
    var avatar: String?
    var followersCount: Int?
    var followsCount: Int?
    var links: [WebLink]?
    var viewer: Viewing?
    var orgs: [Org]?

    struct Viewing: Decodable {
        /// The asker's follow of them, if they follow them.
        var following: String?
        /// The asker's block of them from all its repositories, if it has one.
        var blocking: String?
    }
}

/// What knotless data the asking account holds, before it deletes it.
struct AccountData: Decodable, Sendable {
    let did: String
    let collections: [Count]
    let repos: [Held]
    let spaces: Int

    struct Count: Decodable, Hashable, Sendable {
        let collection: String
        let count: Int
    }

    struct Held: Decodable, Hashable, Sendable {
        let name: String
        let `private`: Bool
        let collaborators: Int
        var members: Int?
        /// How many other accounts use it.
        var sharing: Int { max(collaborators, members ?? 0) }
    }
}

/// One part of deleting an account's data.
struct DeleteStep: Decodable, Sendable {
    let deleted: Int
    let done: Bool
}

/// The accounts the asker blocks, the latest first.
struct BlockList: Decodable, Sendable {
    let blocks: [Block]

    struct Block: Decodable, Identifiable, Sendable {
        let uri: String
        let subject: Person
        /// The one repository it's blocked from, or from all of them with none.
        var repo: From?
        let createdAt: String
        var id: String { uri }

        struct From: Decodable, Sendable {
            let did: String
            let name: String
            var uri: String { "at://\(did)/dev.knotless.repo/\(name)" }
        }
    }
}

struct InboxItem: Decodable, Identifiable, Hashable {
    let uri: String
    let author: Person
    let reason: String
    var reasonSubject: String?
    var repo: RepoRef?
    var title: String?
    var verdict: String?
    var text: String?
    var subjectState: String?
    let isRead: Bool
    let createdAt: String
    var id: String { uri + reason }
}

struct NotificationList: Decodable {
    var notifications: [InboxItem] = []
    var cursor: String?
}

struct FeedEvent: Decodable, Identifiable, Hashable {
    let createdAt: String
    let actor: Person
    let repo: RepoRef
    let action: Action

    /// The pulls and issues among `events`, each once, by what last happened.
    static func recent(_ events: [FeedEvent], most: Int = 8) -> [FeedEvent] {
        var seen = Set<String>()
        return Array(events.filter { event in
            guard let subject = event.action.subject,
                  subject.contains("/dev.knotless.pull/") || subject.contains("/dev.knotless.issue/") else { return false }
            return seen.insert(subject).inserted
        }.prefix(most))
    }

    var id: String { createdAt + actor.did + repo.label + action.summary }

    /// An open union: a kind this version doesn't know still shows as an event.
    struct Action: Decodable, Hashable {
        let type: String
        var subject: String?
        var title: String?
        var state: String?
        var subjectState: String?
        var text: String?
        var verdict: String?
        var ref: String?
        var count: Int?
        var forkOf: RepoRef?

        enum CodingKeys: String, CodingKey {
            case type = "$type", subject, title, state, subjectState, text, verdict, ref, count, forkOf
        }

        var kind: String { type.split(separator: "#").last.map(String.init) ?? type }
        /// Where the subject stands now, or as this event left it on an older server.
        var standing: String? { subjectState ?? state }
        /// What the account whose feed this is did, in a line of its own.
        var yours: String {
            switch kind {
            // A queue's runner merges what you opened, so no event of yours says so.
            case "opened":
                switch subjectState {
                case "merged": "Merged"
                case "closed": "Closed"
                default: "You opened"
                }
            case "changed": "You \(state == "open" ? "reopened" : state ?? "changed")"
            case "commented":
                switch verdict {
                case "approve": "You approved"
                case "changes": "You asked for changes"
                default: "You commented"
                }
            default: "You updated"
            }
        }
        var branch: String? { ref?.replacingOccurrences(of: "refs/heads/", with: "") }

        var summary: String {
            switch kind {
            case "created": forkOf.map { "forked \($0.label)" } ?? "created this repository"
            case "pushed":
                if let tag = ref?.replacing("refs/tags/", with: ""), ref?.hasPrefix("refs/tags/") == true {
                    "pushed tag \(tag)"
                } else if (count ?? 0) == 0 {
                    "pushed to \(branch ?? "a branch")"
                } else {
                    count == 1 ? "pushed 1 commit to \(branch ?? "a branch")" : "pushed \(count ?? 0) commits to \(branch ?? "a branch")"
                }
            case "deleted": "deleted \(branch ?? "a branch")"
            case "opened": "opened"
            case "commented":
                switch verdict {
                case "approve": "approved"
                case "changes": "asked for changes on"
                default: "commented on"
                }
            case "changed":
                switch state {
                case "merged": "merged"
                case "closed": "closed"
                case "open": "reopened"
                default: "changed"
                }
            case "starred": "starred this repository"
            default: "did something new"
            }
        }
    }
}

struct Viewer: Decodable {
    var actor: Person?
    var reauth: Bool?
}

/// What the server asks of clients, of which the app reads its own part.
struct ServerDescription: Decodable {
    struct IOS: Decodable {
        var minimumBuild: Int
        var reason: String?
    }

    var ios: IOS?
}

struct Feed: Decodable {
    var events: [FeedEvent] = []
    var cursor: String?
}

/// A `dev.knotless.theme#palette` as the account keeps it.
struct PaletteRecord: Codable, Equatable, Sendable {
    struct Syntax: Codable, Equatable, Sendable {
        var comments, strings, keywords, numbers, functions, types, tags: String?
    }

    var colorScheme: String?
    var background, panel, soft, border, text, textMuted, textFaint: String?
    var primary, onPrimary, accent, success, danger, done, attention, neutral: String?
    var syntax: Syntax?
}

/// A theme the app is drawn in, as `appearancePref` and its `night` name one:
/// one the pages offer by name, or `custom` with its palette.
struct ThemeHalf: Codable, Equatable, Sendable {
    var theme: String?
    var palette: PaletteRecord?
}

/// How the app is drawn: in one theme, or in a day one while the system is
/// light and a night one while it is dark.
struct ThemeChoice: Codable, Equatable, Sendable {
    var day: ThemeHalf
    var night: ThemeHalf?

    init(day: ThemeHalf, night: ThemeHalf? = nil) {
        self.day = day
        self.night = night
    }
}

/// A theme of the account's own: one it published, or one it added, as Appearance
/// on the pages lists them.
struct OwnTheme: Codable, Equatable, Identifiable, Sendable {
    let uri: String
    let name: String
    let palette: PaletteRecord
    /// Who made it, when that's someone else.
    var maker: String?
    /// When it was made or added, which orders them, the latest first.
    var at: String?

    var id: String { uri }
}

/// What `getPreferences` answers, of the kinds the app acts on.
struct Preferences: Decodable, Sendable {
    let notifications: NotificationChoices?
    /// The theme the pages are drawn in, which the app is drawn in too.
    let appearance: ThemeChoice?
    /// The themes people made that the account added to its own, `themesPref`.
    let themes: [OwnTheme]

    private struct Added: Decodable {
        let uri: String
        let name: String?
        let palette: PaletteRecord
        let addedAt: String?
    }

    private struct Kind: Decodable {
        let choices: NotificationChoices?
        let appearance: ThemeChoice?
        let themes: [OwnTheme]?
        private enum Keys: String, CodingKey { case type = "$type", theme, palette, night, themes }

        init(from decoder: Decoder) throws {
            let c = try decoder.container(keyedBy: Keys.self)
            let type = try c.decodeIfPresent(String.self, forKey: .type)
            choices = try type == NotificationChoices.type ? NotificationChoices(from: decoder) : nil
            if type == "dev.knotless.getPreferences#appearancePref" {
                let day = ThemeHalf(
                    theme: try? c.decodeIfPresent(String.self, forKey: .theme),
                    palette: try? c.decodeIfPresent(PaletteRecord.self, forKey: .palette)
                )
                let night = try? c.decodeIfPresent(ThemeHalf.self, forKey: .night)
                appearance = ThemeChoice(day: day, night: night)
            } else {
                appearance = nil
            }
            if type == "dev.knotless.getPreferences#themesPref" {
                let added = (try? c.decode([Added].self, forKey: .themes)) ?? []
                themes = added.map { OwnTheme(uri: $0.uri, name: $0.name ?? "Untitled", palette: $0.palette, at: $0.addedAt) }
            } else {
                themes = nil
            }
        }
    }

    private enum Keys: String, CodingKey { case preferences }

    init(from decoder: Decoder) throws {
        let kinds = try decoder.container(keyedBy: Keys.self).decode([Kind].self, forKey: .preferences)
        notifications = kinds.lazy.compactMap(\.choices).first
        appearance = kinds.lazy.compactMap(\.appearance).first
        themes = kinds.lazy.compactMap(\.themes).first ?? []
    }
}

/// Which notifications the account hears of: each reason the server gives, on or
/// off, and whether only of what accounts it follows did.
struct NotificationChoices: Decodable, Equatable, Sendable {
    static let type = "dev.knotless.getPreferences#notificationPref"

    struct Reason: Decodable, Equatable, Sendable, Identifiable {
        let reason: String
        var enabled: Bool
        var id: String { reason }
    }

    var reasons: [Reason]
    var followedOnly: Bool?

    /// As `putPreferences` is given it.
    var written: [String: Any] {
        [
            "$type": Self.type,
            "reasons": reasons.map { ["reason": $0.reason, "enabled": $0.enabled] as [String: Any] },
            "followedOnly": followedOnly ?? false,
        ]
    }
}