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

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

Commits in the iOS app open their diff and details

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:ios/commit
ios/Knotless/API/Models.swift+31−0
@@ -206,18 +206,49 @@ struct DiffFile: Decodable, Identifiable, Hashable {
struct CommitAuthor: Decodable, Hashable {
var name: String?
var actor: Person?
var label: String { actor?.handle ?? name ?? "" }
}
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?
}
}
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]?
}
ios/Knotless/Assets.xcassets/oct-check.imageset/check.svgnew+1−0
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"/></svg>
\ No newline at end of file
ios/Knotless/Assets.xcassets/oct-check.imageset/Contents.jsonnew+1−0
@@ -0,0 +1 @@
{"images":[{"filename":"check.svg","idiom":"universal"}],"info":{"author":"xcode","version":1},"properties":{"preserves-vector-representation":true,"template-rendering-intent":"template"}}
\ No newline at end of file
ios/Knotless/Assets.xcassets/oct-copy.imageset/Contents.jsonnew+1−0
@@ -0,0 +1 @@
{"images":[{"filename":"copy.svg","idiom":"universal"}],"info":{"author":"xcode","version":1},"properties":{"preserves-vector-representation":true,"template-rendering-intent":"template"}}
\ No newline at end of file
ios/Knotless/Assets.xcassets/oct-copy.imageset/copy.svgnew+1−0
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/></svg>
\ No newline at end of file
ios/Knotless/Assets.xcassets/oct-file-moved.imageset/Contents.jsonnew+1−0
@@ -0,0 +1 @@
{"images":[{"filename":"file-moved.svg","idiom":"universal"}],"info":{"author":"xcode","version":1},"properties":{"preserves-vector-representation":true,"template-rendering-intent":"template"}}
\ No newline at end of file
ios/Knotless/Assets.xcassets/oct-file-moved.imageset/file-moved.svgnew+1−0
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-3.5a.75.75 0 0 1 0-1.5h3.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073H3.75a.25.25 0 0 0-.25.25v6.5a.75.75 0 0 1-1.5 0v-6.5Z"/><path d="m5.427 15.573 3.146-3.146a.25.25 0 0 0 0-.354L5.427 8.927A.25.25 0 0 0 5 9.104V11.5H.75a.75.75 0 0 0 0 1.5H5v2.396c0 .223.27.335.427.177Z"/></svg>
\ No newline at end of file
ios/Knotless/Views/CommitView.swiftnew+226−0
@@ -0,0 +1,226 @@
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: 5) {
HStack(alignment: .firstTextBaseline, spacing: 10) {
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: 8) {
HStack(spacing: -6) {
ForEach(Array(authors.prefix(2).enumerated()), id: \.offset) { _, person in
Avatar(url: person.actor?.avatar, name: person.label, size: 22)
}
}
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: 12) {
HStack(alignment: .firstTextBaseline, spacing: 10) {
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) }
Text(summary).font(.footnote).foregroundStyle(.secondary)
}
.padding(16)
.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, 60)
}
.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(16)
}
}
}
}
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 {
Section("Commit") {
HStack(spacing: 10) {
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: 20).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: 10) {
Oct(name: "git-commit", size: 20).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: 10) {
Oct(name: "git-pull-request", size: 20).foregroundStyle(.secondary)
Text(pull.title ?? "This commit's pull").lineLimit(2)
}
}
}
}
}
}
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: 12) {
Avatar(url: person.actor?.avatar, name: person.label, size: 32)
Text(person.label).lineLimit(1)
Spacer(minLength: 8)
Text(role).foregroundStyle(.secondary)
}
}
}
ios/Knotless/Views/FilesView.swift+4−20
@@ -41,9 +41,10 @@ private extension View {
}
}
private struct FileSection: View {
struct FileSection: View {
let repo: RepoRef
let uri: String
/// The pull whose rows to ask for, if they aren't in `file`.
var uri: String?
let file: DiffFile
struct Line: Identifiable {
@@ -96,7 +97,7 @@ private struct FileSection: View {
private func load() async {
var source = file
if source.rows.isEmpty, source.note == nil {
if source.rows.isEmpty, source.note == nil, let uri {
let answer: DiffAnswer? = try? await Client.shared.query(
"getPullDiff", ["repo": repo.uri, "uri": uri, "paths": file.path])
guard let found = answer?.files.first else {
@@ -167,20 +168,3 @@ private struct DiffLine: View {
(line.row.kind == "d" ? line.row.old : line.row.new ?? line.row.old).map(String.init) ?? ""
}
}
struct CommitRow: View {
let commit: Commit
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(commit.subject).lineLimit(3)
HStack(spacing: 6) {
Text(String(commit.sha.prefix(7))).font(.footnote.monospaced())
Text(commit.author?.actor?.handle ?? commit.author?.name ?? "")
Text(When.short(commit.date))
}
.font(.footnote)
.foregroundStyle(.secondary)
}
}
}
ios/Knotless/Views/RepoView.swift+2−2
@@ -544,9 +544,9 @@ struct HistoryView: View {
var body: some View {
PagedList<Commit, CommitRow>(empty: "No commits yet.", fetch: { [repo, ref] cursor in
let list: CommitList = try await Client.shared.query(
"listCommits", ["repo": repo.uri, "ref": ref, "subjectOnly": "true", "cursor": cursor])
"listCommits", ["repo": repo.uri, "ref": ref, "limit": "30", "cursor": cursor])
return .init(items: list.commits, cursor: list.cursor)
}) { CommitRow(commit: $0) }
}) { CommitRow(repo: repo, commit: $0) }
.navigationTitle("Commits")
.navigationBarTitleDisplayMode(.inline)
}
ios/Knotless/Views/ThreadView.swift+6−15
ios/Knotless/App.swift+4−0
ios/ModelCheck/main.swift+1−0
ios/look.mjs+2−0