Profile repo rows carry topics, license and last commit !32
merged
cmc wants to merge krz/gitbay-ios:profile-repo-metadata into main
3 files changed, +43 −1
gitbay/Discovery/ProfileViewModel.swift
+19
| @@ -37,11 +37,30 @@ final class ProfileViewModel { |
| 37 | 37 | let visibility: String |
| 38 | 38 | let description: String? |
| 39 | 39 | let archived: Bool? |
| 40 | let topics: [String]? |
| 41 | let license: String? |
| 42 | let defaultBranch: String? |
| 43 | let updated: String? |
| 44 | |
| 45 | enum CodingKeys: String, CodingKey { |
| 46 | case path, visibility, description, archived, topics, license, updated |
| 47 | case defaultBranch = "default_branch" |
| 48 | } |
| 40 | 49 | |
| 41 | 50 | var id: String { path } |
| 42 | 51 | var isPrivate: Bool { visibility == "private" } |
| 43 | 52 | var isArchived: Bool { archived ?? false } |
| 44 | 53 | var name: String { String(path.split(separator: "/").last ?? "") } |
| 54 | |
| 55 | /// "main · MIT · updated 2026-08-30", the line the web's repo |
| 56 | /// rows carry. Parts the server did not report are left out. |
| 57 | var meta: String { |
| 58 | var parts: [String] = [] |
| 59 | if let defaultBranch, !defaultBranch.isEmpty { parts.append(defaultBranch) } |
| 60 | if let license, !license.isEmpty { parts.append(license) } |
| 61 | if let updated, !updated.isEmpty { parts.append("updated \(updated)") } |
| 62 | return parts.joined(separator: " · ") |
| 63 | } |
| 45 | 64 | } |
| 46 | 65 | |
| 47 | 66 | /// One day's contributions. Days with none are omitted, so the |
gitbay/Views/Discovery/ProfileView.swift
+15
| @@ -134,6 +134,21 @@ struct ProfileView: View { |
| 134 | 134 | .foregroundStyle(.secondary) |
| 135 | 135 | .lineLimit(2) |
| 136 | 136 | } |
| 137 | if let topics = repo.topics, !topics.isEmpty { |
| 138 | ScrollView(.horizontal, showsIndicators: false) { |
| 139 | HStack(spacing: 6) { |
| 140 | ForEach(topics, id: \.self) { topic in |
| 141 | GBChip(topic, .gbAccent) |
| 142 | } |
| 143 | } |
| 144 | } |
| 145 | } |
| 146 | if !repo.meta.isEmpty { |
| 147 | Text(repo.meta) |
| 148 | .font(.gbSans(.caption2)) |
| 149 | .foregroundStyle(.secondary) |
| 150 | .lineLimit(1) |
| 151 | } |
| 137 | 152 | } |
| 138 | 153 | } |
| 139 | 154 | } |
gitbayTests/ProfileCommitTests.swift
+9 −1
| @@ -16,7 +16,8 @@ private let userProfileJSON = """ |
| 16 | 16 | {"protocol_version":1,"data":{"name":"cmc","kind":"user",\ |
| 17 | 17 | "description":"Org-Mode · Self-Hosting · Privacy","website":"https://cleberg.net",\ |
| 18 | 18 | "orgs":[{"name":"krz","role":"admin"},{"name":"audit-labs","role":"admin"}],\ |
| 19 | | "repos":[{"path":"cmc/notes","visibility":"private","description":"notes"},\ |
| 19 | "repos":[{"path":"cmc/notes","visibility":"private","description":"notes",\ |
| 20 | "topics":["org","writing"],"license":"MIT","default_branch":"main","updated":"2026-08-28"},\ |
| 20 | 21 | {"path":"cmc/cv","visibility":"public"}],\ |
| 21 | 22 | "activity":[{"date":"2026-08-01","count":3},{"date":"2026-08-02","count":12}],\ |
| 22 | 23 | "activity_total":15},"exit_code":0} |
| @@ -42,6 +43,13 @@ struct ProfileAggregateTests { |
| 42 | 43 | #expect(profile.orgs?.map(\.name) == ["krz", "audit-labs"]) |
| 43 | 44 | #expect(profile.repos.map(\.path) == ["cmc/notes", "cmc/cv"]) |
| 44 | 45 | #expect(profile.repos[0].isPrivate) |
| 46 | // Repo rows carry the listing metadata, so a profile lists |
| 47 | // repositories the way explore does. |
| 48 | #expect(profile.repos[0].topics == ["org", "writing"]) |
| 49 | #expect(profile.repos[0].meta == "main · MIT · updated 2026-08-28") |
| 50 | // A row the server reported without any of it still renders. |
| 51 | #expect(profile.repos[1].topics == nil) |
| 52 | #expect(profile.repos[1].meta.isEmpty) |
| 45 | 53 | #expect(profile.activityTotal == 15) |
| 46 | 54 | // One read builds the whole page. |
| 47 | 55 | #expect(stub.seen.count == 1) |