Commit 39829bcd36
Verified · cmc
Layout: unified · split
gitbay/Discovery/ProfileViewModel.swift +21 −7
| @@ -146,19 +146,26 @@ final class ProfileViewModel { | ||
| 146 | 146 | /// `profile/README.md`, the same starter the web's settings page |
| 147 | 147 | /// writes, so the file editor has something to open. The repository |
| 148 | 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 | 155 | func createAboutFile() async -> Bool { |
| 151 | 156 | working = true |
| 152 | 157 | actionError = nil |
| 153 | 158 | defer { working = false } |
| 154 | 159 | do { |
| 160 | var ref = "main" | |
| 155 | 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 | 164 | } catch let error as GitbayError where error.isEmptyState { |
| 158 | 165 | try await client.run(["repo", "create", aboutRepo]) |
| 159 | 166 | } |
| 160 | 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 | 169 | "--message", "add profile about", "--file", "-"], |
| 163 | 170 | stdin: "# \(name)\n\nThis is the about text on your profile.\n") |
| 164 | 171 | await load() |
| @@ -171,15 +178,22 @@ final class ProfileViewModel { | ||
| 171 | 178 | return false |
| 172 | 179 | } |
| 173 | 180 | |
| 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? | |
| 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 | 192 | working = true |
| 179 | 193 | actionError = nil |
| 180 | 194 | defer { working = false } |
| 181 | 195 | do { |
| 182 | try await client.run(argv, stdin: stdin) | |
| 196 | try await client.run(argv) | |
| 183 | 197 | await load() |
| 184 | 198 | } catch let error as GitbayError { |
| 185 | 199 | actionError = error.userFacingMessage |
gitbayTests/ProfileCommitTests.swift +61
| @@ -230,4 +230,65 @@ struct AboutFileTests { | ||
| 230 | 230 | let (client, _) = try makeClient() |
| 231 | 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 | } |