Commit 0a519cfa8f

0a519cfa8fe7e55bb7b4c4721e8d13c1751fea65

parent: c091e39943

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-22 17:56 UTC

profile: set description, website and links only

gitbay v1.31.0 moved the about text into <owner>/.gitbay, and profile
set and org profile refuse --about-format and --file as unknown flags,
so every save failed with exit 2.

Layout: unified · split

gitbay/Discovery/ProfileEdit.swift +4 −17
@@ -6,6 +6,9 @@ import Foundation
66/// the current profile, so every other field is also sent every time,
77/// including empty ones: `--description ""` clears, omitting the flag
88/// would leave the old value alone.
9///
10/// The about text is not here. It is a file in `<owner>/.gitbay`, written
11/// through the file editor like any other file.
912nonisolated struct ProfileEdit: Equatable, Sendable {
1013
1114 nonisolated struct Link: Equatable, Sendable, Identifiable {
@@ -16,27 +19,17 @@ nonisolated struct ProfileEdit: Equatable, Sendable {
1619
1720 var description: String
1821 var website: String
19 var about: String
20 var aboutFormat: String
2122 var links: [Link]
2223
23 init(
24 description: String = "", website: String = "",
25 about: String = "", aboutFormat: String = "md",
26 links: [Link] = []
27 ) {
24 init(description: String = "", website: String = "", links: [Link] = []) {
2825 self.description = description
2926 self.website = website
30 self.about = about
31 self.aboutFormat = aboutFormat
3227 self.links = links
3328 }
3429
3530 init(from profile: ProfileViewModel.Profile) {
3631 description = profile.description ?? ""
3732 website = profile.website ?? ""
38 about = profile.about ?? ""
39 aboutFormat = profile.aboutFormat ?? "md"
4033 links = (profile.links ?? []).map { Link(label: $0.label ?? "", url: $0.url) }
4134 }
4235
@@ -56,7 +49,6 @@ nonisolated struct ProfileEdit: Equatable, Sendable {
5649 return nil
5750 }
5851
59 /// Everything except the about text, which travels over stdin.
6052 func flags() -> [String] {
6153 var flags = ["--description", description, "--website", website]
6254 if links.isEmpty {
@@ -67,11 +59,6 @@ nonisolated struct ProfileEdit: Equatable, Sendable {
6759 flags += ["--link", label.isEmpty ? link.url : "\(label)|\(link.url)"]
6860 }
6961 }
70 flags += ["--about-format", aboutFormat, "--file", "-"]
7162 return flags
7263 }
73
74 /// The about text, sent over stdin — never in argv, since it is
75 /// long-form and may hold newlines.
76 var aboutStdin: String { about }
7764}
gitbay/Discovery/ProfileViewModel.swift +2 −3
@@ -134,12 +134,11 @@ final class ProfileViewModel {
134134 }
135135
136136 /// `profile set` for the caller's own profile, `org profile <org>` for
137 /// an organization; both take the same flags. The about text travels
138 /// over stdin, never argv, since it is long-form and may carry newlines.
137 /// an organization; both take the same flags.
139138 func saveProfile(_ edit: ProfileEdit) async -> Bool {
140139 guard edit.validationError == nil else { return false }
141140 let command = state.value?.isOrg == true ? ["org", "profile", name] : ["profile", "set"]
142 await perform(command + edit.flags(), stdin: edit.aboutStdin)
141 await perform(command + edit.flags())
143142 return actionError == nil
144143 }
145144
gitbay/Views/Discovery/ProfileEditSheet.swift +4 −22
@@ -1,10 +1,9 @@
11import SwiftUI
22
3/// `profile set` — always the caller's own. Description and website are
4/// single lines; about is long-form prose in whichever format the
5/// profile already carries, since switching formats reinterprets text
6/// that was already written; links are capped at five, matching the
7/// server.
3/// `profile set`, or `org profile` for an organization. Description and
4/// website are single lines; links are capped at five, matching the
5/// server. The about text is a file and is edited from the profile
6/// screen, not here.
87struct ProfileEditSheet: View {
98
109 let model: ProfileViewModel
@@ -35,23 +34,6 @@ struct ProfileEditSheet: View {
3534 .textInputAutocapitalization(.never)
3635 .accessibilityIdentifier("profile-edit-website")
3736 }
38 Section {
39 Picker("Format", selection: $edit.aboutFormat) {
40 Text("Markdown").tag("md")
41 Text("Org").tag("org")
42 }
43 .pickerStyle(.segmented)
44 .accessibilityIdentifier("profile-edit-format")
45 TextEditor(text: $edit.about)
46 .font(.gbMono(.caption))
47 .frame(minHeight: 160)
48 .autocorrectionDisabled()
49 .accessibilityIdentifier("profile-edit-about")
50 } header: {
51 Text("About")
52 } footer: {
53 Text("Changing the format reinterprets text that is already written.")
54 }
5537 linksSection
5638 if let error = localError ?? model.actionError {
5739 Section {
gitbayTests/ProfileEditTests.swift +12 −41
@@ -6,11 +6,9 @@ struct ProfileEditFlagTests {
66
77 private func edit(
88 description: String = "", website: String = "",
9 about: String = "", aboutFormat: String = "md",
109 links: [ProfileEdit.Link] = []
1110 ) -> ProfileEdit {
12 ProfileEdit(description: description, website: website,
13 about: about, aboutFormat: aboutFormat, links: links)
11 ProfileEdit(description: description, website: website, links: links)
1412 }
1513
1614 @Test func everyFieldSendsItsFlag() {
@@ -59,19 +57,14 @@ struct ProfileEditFlagTests {
5957 #expect(flags.contains("|https://a.test") == false)
6058 }
6159
62 /// Long-form text with newlines belongs in stdin, not argv.
63 @Test func theAboutTextGoesToStdinNotArgv() {
64 let e = edit(about: "line one\n\nline two", aboutFormat: "org")
65 #expect(e.flags().contains { $0.contains("line one") } == false)
66 #expect(e.flags().contains("--file"))
67 #expect(e.flags().contains("-"))
68 #expect(e.aboutStdin == "line one\n\nline two")
69 }
70
71 @Test func theAboutFormatAccompaniesTheText() {
72 let flags = edit(about: "x", aboutFormat: "org").flags()
73 let i = try! #require(flags.firstIndex(of: "--about-format"))
74 #expect(flags[i + 1] == "org")
60 /// Since gitbay v1.31.0 the about text is a file in <owner>/.gitbay,
61 /// and `profile set` refuses any of these as an unknown flag (exit 2).
62 @Test func noAboutFlagIsSent() {
63 let flags = edit(description: "hi", website: "https://x.test",
64 links: [.init(label: "a", url: "https://a.test")]).flags()
65 #expect(!flags.contains("--about"))
66 #expect(!flags.contains("--about-format"))
67 #expect(!flags.contains("--file"))
7568 }
7669
7770 @Test func moreThanFiveLinksIsInvalid() {
@@ -103,36 +96,17 @@ struct ProfileEditFromProfileTests {
10396 "repos":[],"activity_total":0}
10497 """
10598
106 /// The real profile on gitbay.org is org, with tables and links that
107 /// render as flat text under markdown — the stored format must round
108 /// trip, not default away from what the server holds.
109 @Test func seedsFromAnOrgProfile() throws {
99 @Test func seedsFromTheProfile() throws {
110100 let data = try #require(orgProfileJSON.data(using: .utf8))
111101 let profile = try JSONDecoder().decode(ProfileViewModel.Profile.self, from: data)
112102
113103 let edit = ProfileEdit(from: profile)
114104
115 #expect(edit.aboutFormat == "org")
116105 #expect(edit.description == "about me")
117106 #expect(edit.website == "https://cleberg.net")
118 #expect(edit.about == "* heading\n\n| a | b |")
119107 #expect(edit.links.map(\.url) == ["https://a.test", "https://b.test"])
120108 #expect(edit.links.map(\.label) == ["", "blog"])
121109 }
122
123 /// Absent `about_format`, seeding falls back to markdown.
124 @Test func seedingFallsBackToMarkdownWhenFormatIsAbsent() throws {
125 let json = """
126 {"name":"cmc","kind":"user",\
127 "description":"hi","repos":[],"activity_total":0}
128 """
129 let data = try #require(json.data(using: .utf8))
130 let profile = try JSONDecoder().decode(ProfileViewModel.Profile.self, from: data)
131
132 let edit = ProfileEdit(from: profile)
133
134 #expect(edit.aboutFormat == "md")
135 }
136110}
137111
138112private func makeClient() throws -> (GitbayClient, StubProtocol.Box) {
@@ -170,14 +144,13 @@ struct ProfileSaveTests {
170144 /// The write, then the reload `profile set` triggers — one POST, one
171145 /// GET. Both need a stub or the flow ends up looking like a refusal
172146 /// even when the write itself succeeded.
173 @Test func savesTheFullArgvWithAboutOnlyInStdin() async throws {
147 @Test func savesScalarsAndLinksWithNoStdin() async throws {
174148 let (client, stub) = try makeClient()
175149 stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
176150 stub.enqueue(.init(status: 200, json: profileJSON, match: "argv=show"))
177151 let model = ProfileViewModel(client: client, name: "cmc")
178152 let edit = ProfileEdit(
179153 description: "hi", website: "https://cleberg.net",
180 about: "long form\n\nprose", aboutFormat: "org",
181154 links: [.init(label: "blog", url: "https://b.test")]
182155 )
183156
@@ -190,10 +163,8 @@ struct ProfileSaveTests {
190163 "profile", "set",
191164 "--description", "hi", "--website", "https://cleberg.net",
192165 "--link", "blog|https://b.test",
193 "--about-format", "org", "--file", "-",
194166 ])
195 #expect(stdin == "long form\n\nprose")
196 #expect(argv.contains { $0.contains("long form") } == false)
167 #expect(stdin == nil)
197168 }
198169
199170 /// An org's profile is written by `org profile <org>`, with the same