Commit 39829bcd36

39829bcd365d15fd0e4337253d7036e00ae07555

parent: c8e77aa79f

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-22 18:50 UTC

profile: commit the about starter to the repository's default branch

The about file is only read from <owner>/.gitbay's default branch,
which is not always main for a repository that already exists — the
first branch ever pushed becomes the default. repo show now decodes
default_branch and the commit targets it, falling back to main when
the repository is fresh or reports none.

Also drops the stdin parameter from perform(), unused since profile
set stopped taking an about flag, and adds coverage for the create
flow's failure paths: a repo show refusal other than not-found stops
before any write, and a commit-file failure after repo create still
surfaces and skips the reload.

Layout: unified · split

gitbay/Discovery/ProfileViewModel.swift +21 −7
@@ -146,19 +146,26 @@ final class ProfileViewModel {
146 /// `profile/README.md`, the same starter the web's settings page 146 /// `profile/README.md`, the same starter the web's settings page
147 /// writes, so the file editor has something to open. The repository 147 /// writes, so the file editor has something to open. The repository
148 /// may already exist without the file, and creating it again would 148 /// may already exist without the file, and creating it again would
149 /// fail, so it is looked up first. 149 /// fail, so it is looked up first — and when it does exist, the
150 /// commit targets its default branch: the about file is only read
151 /// from there, and an existing repository's default is not
152 /// necessarily `main` (the first branch ever pushed becomes the
153 /// default). A repository this call creates is fresh, so its default
154 /// is `main`.
150 func createAboutFile() async -> Bool { 155 func createAboutFile() async -> Bool {
151 working = true 156 working = true
152 actionError = nil 157 actionError = nil
153 defer { working = false } 158 defer { working = false }
154 do { 159 do {
160 var ref = "main"
155 do { 161 do {
156 _ = try await client.read(["repo", "show", aboutRepo], as: Exists.self) 162 let existing = try await client.read(["repo", "show", aboutRepo], as: Exists.self)
163 ref = existing.defaultBranch ?? "main"
157 } catch let error as GitbayError where error.isEmptyState { 164 } catch let error as GitbayError where error.isEmptyState {
158 try await client.run(["repo", "create", aboutRepo]) 165 try await client.run(["repo", "create", aboutRepo])
159 } 166 }
160 try await client.run( 167 try await client.run(
161 ["repo", "commit-file", aboutRepo, "profile/README.md", "--ref", "main", 168 ["repo", "commit-file", aboutRepo, "profile/README.md", "--ref", ref,
162 "--message", "add profile about", "--file", "-"], 169 "--message", "add profile about", "--file", "-"],
163 stdin: "# \(name)\n\nThis is the about text on your profile.\n") 170 stdin: "# \(name)\n\nThis is the about text on your profile.\n")
164 await load() 171 await load()
@@ -171,15 +178,22 @@ final class ProfileViewModel {
171 return false 178 return false
172 } 179 }
173 180
174 /// `repo show` is read only to learn whether the repository exists. 181 /// `repo show` is read only to learn whether the repository exists,
175 nonisolated private struct Exists: Decodable, Sendable {} 182 /// and which branch is its default.
183 nonisolated private struct Exists: Decodable, Sendable {
184 let defaultBranch: String?
176 185
177 private func perform(_ argv: [String], stdin: String? = nil) async { 186 enum CodingKeys: String, CodingKey {
187 case defaultBranch = "default_branch"
188 }
189 }
190
191 private func perform(_ argv: [String]) async {
178 working = true 192 working = true
179 actionError = nil 193 actionError = nil
180 defer { working = false } 194 defer { working = false }
181 do { 195 do {
182 try await client.run(argv, stdin: stdin) 196 try await client.run(argv)
183 await load() 197 await load()
184 } catch let error as GitbayError { 198 } catch let error as GitbayError {
185 actionError = error.userFacingMessage 199 actionError = error.userFacingMessage
gitbayTests/ProfileCommitTests.swift +61
@@ -230,4 +230,65 @@ struct AboutFileTests {
230 let (client, _) = try makeClient() 230 let (client, _) = try makeClient()
231 #expect(ProfileViewModel(client: client, name: "cmc").aboutRepo == "cmc/.gitbay") 231 #expect(ProfileViewModel(client: client, name: "cmc").aboutRepo == "cmc/.gitbay")
232 } 232 }
233
234 /// The about file is only read from the repository's default branch,
235 /// which is not always `main` for a repository that already exists.
236 @Test func commitsToTheExistingRepositorysDefaultBranch() async throws {
237 let (client, stub) = try makeClient()
238 stub.enqueue(.init(status: 200, json:
239 #"{"protocol_version":1,"data":{"path":"cmc/.gitbay","default_branch":"master"},"exit_code":0}"#,
240 match: "argv=repo&argv=show"))
241 stub.enqueue(.init(status: 200, json: okCmdJSON, match: "cmd"))
242 stub.enqueue(.init(status: 200, json: userProfileJSON, match: "argv=profile&argv=show"))
243 let model = ProfileViewModel(client: client, name: "cmc")
244
245 let created = await model.createAboutFile()
246
247 #expect(created)
248 let writes = try postArgv(stub)
249 #expect(writes.count == 1)
250 #expect(writes[0].0 == [
251 "repo", "commit-file", "cmc/.gitbay", "profile/README.md", "--ref", "master",
252 "--message", "add profile about", "--file", "-",
253 ])
254 }
255
256 /// Any failure but not-found on `repo show` stops the flow before any
257 /// write — a permission refusal must not fall through to creating or
258 /// committing into a repository the caller cannot even read.
259 @Test func aNonNotFoundShowFailureStopsBeforeAnyWrite() async throws {
260 let (client, stub) = try makeClient()
261 stub.enqueue(.init(status: 403, json:
262 #"{"protocol_version":1,"error":"denied","exit_code":4}"#,
263 match: "argv=repo&argv=show"))
264 let model = ProfileViewModel(client: client, name: "krz")
265
266 let created = await model.createAboutFile()
267
268 #expect(created == false)
269 #expect(model.actionError?.isEmpty == false)
270 #expect(try postArgv(stub).isEmpty)
271 }
272
273 /// A create that succeeds but a commit that then fails must not
274 /// report success or reload — the file was never written.
275 @Test func aFailedCommitAfterCreateStopsAndDoesNotReload() async throws {
276 let (client, stub) = try makeClient()
277 stub.enqueue(.init(status: 404, json:
278 #"{"protocol_version":1,"error":"no repository krz/.gitbay","exit_code":3}"#,
279 match: "argv=repo&argv=show"))
280 stub.enqueue(.init(status: 200, json: okCmdJSON, match: "cmd"))
281 stub.enqueue(.init(status: 200, json:
282 #"{"protocol_version":1,"error":"disk full","exit_code":1}"#,
283 match: "cmd"))
284 let model = ProfileViewModel(client: client, name: "krz")
285
286 let created = await model.createAboutFile()
287
288 #expect(created == false)
289 #expect(model.actionError?.isEmpty == false)
290 let writes = try postArgv(stub)
291 #expect(writes.count == 2)
292 #expect(stub.seen.contains { $0.url.query()?.contains("argv=profile&argv=show") == true } == false)
293 }
233} 294}