Commit d56d8c6c96

d56d8c6c965225c838efcb347718042e3f13afd9

parent: c93e0d11e9

Unsigned

cmc <hello@cleberg.net> · 2026-08-30 06:41 UTC

Profile repo rows carry topics, license and last commit

Explore rendered topic chips and a profile did not, so the same
repository read differently on two screens. The cause was the payload:
profile show reported only path, visibility, description and archived.
krz/gitbay!122 added the rest, so Repo decodes it and the rows render
topic chips plus "branch · license · updated", matching explore and the
web's repo rows.

Ref krz/gitbay#47

Layout: unified · split

gitbay/Discovery/ProfileViewModel.swift +19
@@ -37,11 +37,30 @@ final class ProfileViewModel {
37 let visibility: String 37 let visibility: String
38 let description: String? 38 let description: String?
39 let archived: Bool? 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 var id: String { path } 50 var id: String { path }
42 var isPrivate: Bool { visibility == "private" } 51 var isPrivate: Bool { visibility == "private" }
43 var isArchived: Bool { archived ?? false } 52 var isArchived: Bool { archived ?? false }
44 var name: String { String(path.split(separator: "/").last ?? "") } 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 /// One day's contributions. Days with none are omitted, so the 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 .foregroundStyle(.secondary) 134 .foregroundStyle(.secondary)
135 .lineLimit(2) 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 {"protocol_version":1,"data":{"name":"cmc","kind":"user",\ 16 {"protocol_version":1,"data":{"name":"cmc","kind":"user",\
17 "description":"Org-Mode · Self-Hosting · Privacy","website":"https://cleberg.net",\ 17 "description":"Org-Mode · Self-Hosting · Privacy","website":"https://cleberg.net",\
18 "orgs":[{"name":"krz","role":"admin"},{"name":"audit-labs","role":"admin"}],\ 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 {"path":"cmc/cv","visibility":"public"}],\ 21 {"path":"cmc/cv","visibility":"public"}],\
21 "activity":[{"date":"2026-08-01","count":3},{"date":"2026-08-02","count":12}],\ 22 "activity":[{"date":"2026-08-01","count":3},{"date":"2026-08-02","count":12}],\
22 "activity_total":15},"exit_code":0} 23 "activity_total":15},"exit_code":0}
@@ -42,6 +43,13 @@ struct ProfileAggregateTests {
42 #expect(profile.orgs?.map(\.name) == ["krz", "audit-labs"]) 43 #expect(profile.orgs?.map(\.name) == ["krz", "audit-labs"])
43 #expect(profile.repos.map(\.path) == ["cmc/notes", "cmc/cv"]) 44 #expect(profile.repos.map(\.path) == ["cmc/notes", "cmc/cv"])
44 #expect(profile.repos[0].isPrivate) 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 #expect(profile.activityTotal == 15) 53 #expect(profile.activityTotal == 15)
46 // One read builds the whole page. 54 // One read builds the whole page.
47 #expect(stub.seen.count == 1) 55 #expect(stub.seen.count == 1)