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 {
146146 /// `profile/README.md`, the same starter the web's settings page
147147 /// writes, so the file editor has something to open. The repository
148148 /// 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`.
150155 func createAboutFile() async -> Bool {
151156 working = true
152157 actionError = nil
153158 defer { working = false }
154159 do {
160 var ref = "main"
155161 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"
157164 } catch let error as GitbayError where error.isEmptyState {
158165 try await client.run(["repo", "create", aboutRepo])
159166 }
160167 try await client.run(
161 ["repo", "commit-file", aboutRepo, "profile/README.md", "--ref", "main",
168 ["repo", "commit-file", aboutRepo, "profile/README.md", "--ref", ref,
162169 "--message", "add profile about", "--file", "-"],
163170 stdin: "# \(name)\n\nThis is the about text on your profile.\n")
164171 await load()
@@ -171,15 +178,22 @@ final class ProfileViewModel {
171178 return false
172179 }
173180
174 /// `repo show` is read only to learn whether the repository exists.
175 nonisolated private struct Exists: Decodable, Sendable {}
181 /// `repo show` is read only to learn whether the repository exists,
182 /// and which branch is its default.
183 nonisolated private struct Exists: Decodable, Sendable {
184 let defaultBranch: String?
176185
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 {
178192 working = true
179193 actionError = nil
180194 defer { working = false }
181195 do {
182 try await client.run(argv, stdin: stdin)
196 try await client.run(argv)
183197 await load()
184198 } catch let error as GitbayError {
185199 actionError = error.userFacingMessage
gitbayTests/ProfileCommitTests.swift +61
@@ -230,4 +230,65 @@ struct AboutFileTests {
230230 let (client, _) = try makeClient()
231231 #expect(ProfileViewModel(client: client, name: "cmc").aboutRepo == "cmc/.gitbay")
232232 }
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 }
233294}