╭ admin.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const evt = @import("../../event.zig"); 3 const fork = @import("../../fork.zig"); 4 const xit = @import("xit"); 5 const rp = xit.repo; 6 const hash = xit.hash; 7 8 const fork_repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 9 const author = evt.CommitAuthor{ .name = "haxy", .email = "user@haxy" }; 10 11 test "user and repo" { 12 const io = std.testing.io; 13 const allocator = std.testing.allocator; 14 15 // create the temp dir 16 var temp = std.testing.tmpDir(.{}); 17 defer temp.cleanup(); 18 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 19 defer allocator.free(temp_path); 20 21 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 22 const Repo = rp.Repo(.xit, repo_opts); 23 const admin_path = try std.fs.path.join(allocator, &.{ temp_path, "admin" }); 24 defer allocator.free(admin_path); 25 var repo = try Repo.init(io, allocator, .{ .path = admin_path }); 26 defer repo.deinit(io, allocator); 27 const user_path = try std.fs.path.join(allocator, &.{ temp_path, "user" }); 28 defer allocator.free(user_path); 29 var user_repo = try Repo.init(io, allocator, .{ .path = user_path }); 30 defer user_repo.deinit(io, allocator); 31 32 var arena = std.heap.ArenaAllocator.init(allocator); 33 defer arena.deinit(); 34 35 // 36 // define test events 37 // 38 39 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 40 const user_event_id = evt.EventWithId.randomId(prng.random()); 41 const repo_event_id = evt.EventWithId.randomId(prng.random()); 42 43 var first_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 44 const first_password_hash = try evt.User.hashPassword("correct horse battery staple", &first_password_hash_buf, io); 45 46 var second_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 47 const second_password_hash = try evt.User.hashPassword("Tr0ub4dor&3", &second_password_hash_buf, io); 48 49 const events_to_consume = [_]evt.EventWithId{ 50 .{ 51 .id = std.fmt.bytesToHex(user_event_id, .lower), 52 .author = author, 53 .event = .{ 54 .user = .{ 55 .name = "alice", 56 .email = "alice@example.test", 57 .password_hash = first_password_hash, 58 }, 59 }, 60 }, 61 // this event edits the previous one because it has the same id 62 .{ 63 .id = std.fmt.bytesToHex(user_event_id, .lower), 64 .author = author, 65 .event = .{ 66 .user = .{ 67 .name = "alice", 68 .email = "alice@example.test", 69 .password_hash = second_password_hash, 70 }, 71 }, 72 }, 73 }; 74 const repo_event = evt.EventWithId{ 75 .id = std.fmt.bytesToHex(repo_event_id, .lower), 76 .author = author, 77 .event = .{ 78 .repo = .{ 79 .user_id = &user_event_id, 80 .name = "ziglings", 81 .description = "Learn the Zig programming language by fixing tiny broken programs", 82 }, 83 }, 84 }; 85 86 // commit and consume the seed events. users go in the admin repo, repos in 87 // their owner's user repo. 88 try evt.consume(.server, .admin, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume); 89 try evt.consume(.server, .user, .xit, repo_opts, io, allocator, &user_repo, evt.events_ref, &.{repo_event}); 90 91 { 92 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 93 94 // get the map of users 95 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 96 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 97 98 // get the user out of the map that was edited 99 const user_cursor = try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 100 const user_map = try Repo.DB.HashMap(.read_only).init(user_cursor); 101 const user_event = try evt.read(evt.User.Record, Repo.DB, repo_opts.hash, &arena, user_map); 102 103 // the password was correctly edited 104 try std.testing.expectEqualStrings(events_to_consume[1].event.user.?.password_hash, user_event.event.password_hash); 105 } 106 107 { 108 const haxy_moment = try evt.currentMoment(repo_opts, &user_repo); 109 110 // get the map of repos 111 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 112 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 113 114 // get the repo out of the map 115 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 116 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 117 const repo_record = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); 118 119 try std.testing.expectEqualSlices(u8, &user_event_id, repo_record.event.user_id); 120 try std.testing.expectEqualStrings("ziglings", repo_record.event.name); 121 122 // get the repos created by the user 123 const user_repos_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Repo.active_id_set_key)) orelse return error.NotFound; 124 const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); 125 126 try std.testing.expectEqual(1, try user_repos.count()); 127 128 // the set is keyed by orderKeyDesc([created-order][event-id]) 129 const order_key = evt.orderKeyDesc(repo_record.created_order, &repo_event_id); 130 try std.testing.expect(try user_repos.contains(&order_key)); 131 132 // the repo resolves by its name alone 133 const found = (try evt.Repo.readByName(Repo.DB, repo_opts.hash, haxy_moment, &arena, "ziglings")) orelse return error.NotFound; 134 try std.testing.expectEqualSlices(u8, &repo_event_id, &found.event_id); 135 } 136 137 // 138 // remove the repo 139 // 140 141 const events_to_consume2 = [_]evt.EventWithId{ 142 .{ 143 .id = std.fmt.bytesToHex(repo_event_id, .lower), 144 .author = author, 145 .event = .{ .repo = null }, 146 }, 147 }; 148 149 // commit and consume the removal 150 try evt.consume(.server, .user, .xit, repo_opts, io, allocator, &user_repo, evt.events_ref, &events_to_consume2); 151 152 { 153 const haxy_moment = try evt.currentMoment(repo_opts, &user_repo); 154 155 // get the map of repos 156 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 157 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 158 159 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 160 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 161 const repo_record = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); 162 try std.testing.expect(repo_record.removed); 163 164 // get the repos created by the user 165 const user_repos_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Repo.active_id_set_key)) orelse return error.NotFound; 166 const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); 167 168 // removing the repo emptied the user's set 169 try std.testing.expectEqual(0, try user_repos.count()); 170 try std.testing.expectEqual(null, try evt.Repo.readByName(Repo.DB, repo_opts.hash, haxy_moment, &arena, "ziglings")); 171 } 172 173 // a non-null payload restores the repo and its indexes 174 try evt.consume(.server, .user, .xit, repo_opts, io, allocator, &user_repo, evt.events_ref, &.{repo_event}); 175 176 { 177 const haxy_moment = try evt.currentMoment(repo_opts, &user_repo); 178 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 179 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 180 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 181 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 182 const repo_record = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); 183 try std.testing.expect(!repo_record.removed); 184 185 const user_repos_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Repo.active_id_set_key)) orelse return error.NotFound; 186 const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); 187 try std.testing.expectEqual(1, try user_repos.count()); 188 } 189 190 // 191 // remove the user 192 // 193 194 const events_to_consume3 = [_]evt.EventWithId{ 195 .{ 196 .id = std.fmt.bytesToHex(user_event_id, .lower), 197 .author = author, 198 .event = .{ .user = null }, 199 }, 200 }; 201 202 // commit and consume the removal 203 try evt.consume(.server, .admin, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume3); 204 205 { 206 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 207 208 // get the map of users 209 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 210 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 211 212 const user_cursor = try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 213 const user_map = try Repo.DB.HashMap(.read_only).init(user_cursor); 214 const user_event = try evt.read(evt.User.Record, Repo.DB, repo_opts.hash, &arena, user_map); 215 try std.testing.expect(user_event.removed); 216 } 217 } 218 219 test "repos and users paginate newest first" { 220 const io = std.testing.io; 221 const allocator = std.testing.allocator; 222 223 var temp = std.testing.tmpDir(.{}); 224 defer temp.cleanup(); 225 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 226 defer allocator.free(temp_path); 227 228 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 229 const Repo = rp.Repo(.xit, repo_opts); 230 const admin_path = try std.fs.path.join(allocator, &.{ temp_path, "admin" }); 231 defer allocator.free(admin_path); 232 var admin = try Repo.init(io, allocator, .{ .path = admin_path }); 233 defer admin.deinit(io, allocator); 234 const user_path = try std.fs.path.join(allocator, &.{ temp_path, "user" }); 235 defer allocator.free(user_path); 236 var repo = try Repo.init(io, allocator, .{ .path = user_path }); 237 defer repo.deinit(io, allocator); 238 239 var arena = std.heap.ArenaAllocator.init(allocator); 240 defer arena.deinit(); 241 242 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 243 const user_id = evt.EventWithId.randomId(prng.random()); 244 var repo_ids: [4][evt.event_id_size]u8 = undefined; 245 for (&repo_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 246 247 var pw_buf: [evt.User.password_hash_max_len]u8 = undefined; 248 const pw = try evt.User.hashPassword("pw", &pw_buf, io); 249 250 // asserts the repo-id-set holds exactly `expected` ids in that order 251 const Check = struct { 252 fn order( 253 comptime DB: type, 254 comptime hash_kind: hash.HashKind, 255 moment: DB.HashMap(.read_only), 256 expected: []const *const [evt.event_id_size]u8, 257 ) !void { 258 const cursor = try moment.getCursor(hash.hashInt(hash_kind, "repo-id-set")) orelse return error.NotFound; 259 const set = try DB.SortedSet(.read_only).init(cursor); 260 try std.testing.expectEqual(expected.len, try set.count()); 261 for (expected, 0..) |id, i| { 262 const kv = (try set.getIndexKeyValuePair(@intCast(i))) orelse return error.NotFound; 263 // the key is orderKeyDesc ([created-order][event-id]); its trailing bytes are the id 264 var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; 265 _ = try kv.key_cursor.readBytes(&order_key); 266 try std.testing.expectEqualSlices(u8, id, order_key[@sizeOf(u64)..]); 267 } 268 } 269 }; 270 271 // one user in the admin repo, then four repos in creation order with 272 // deliberately skewed timestamps in the user's repo 273 try evt.consume(.server, .admin, .xit, repo_opts, io, allocator, &admin, evt.events_ref, &.{ 274 .{ .id = std.fmt.bytesToHex(user_id, .lower), .author = author, .timestamp = 100, .event = .{ .user = .{ .name = "alice", .email = "alice@example.test", .password_hash = pw } } }, 275 }); 276 const events = [_]evt.EventWithId{ 277 .{ .id = std.fmt.bytesToHex(repo_ids[0], .lower), .author = author, .timestamp = 10_000, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo0", .description = "d0" } } }, 278 .{ .id = std.fmt.bytesToHex(repo_ids[1], .lower), .author = author, .timestamp = 10, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo1", .description = "d1" } } }, 279 .{ .id = std.fmt.bytesToHex(repo_ids[2], .lower), .author = author, .timestamp = 5_000, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo2", .description = "d2" } } }, 280 .{ .id = std.fmt.bytesToHex(repo_ids[3], .lower), .author = author, .timestamp = 20, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo3", .description = "d3" } } }, 281 }; 282 try evt.consume(.server, .user, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &events); 283 284 { 285 const moment = try evt.currentMoment(repo_opts, &repo); 286 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[1], &repo_ids[0] }); 287 288 // the single user shows up in its own ordered set 289 const admin_moment = try evt.currentMoment(repo_opts, &admin); 290 const ucur = try admin_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id-set")) orelse return error.NotFound; 291 const uset = try Repo.DB.SortedSet(.read_only).init(ucur); 292 try std.testing.expectEqual(1, try uset.count()); 293 } 294 295 // removing repo1 retains its place in the canonical order 296 try evt.consume(.server, .user, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{ 297 .{ .id = std.fmt.bytesToHex(repo_ids[1], .lower), .author = author, .timestamp = 200, .event = .{ .repo = null } }, 298 }); 299 { 300 const moment = try evt.currentMoment(repo_opts, &repo); 301 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[1], &repo_ids[0] }); 302 } 303 304 // update repo0 at a later timestamp -> keeps its original place 305 try evt.consume(.server, .user, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{ 306 .{ .id = std.fmt.bytesToHex(repo_ids[0], .lower), .author = author, .timestamp = 300, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo0", .description = "updated" } } }, 307 }); 308 { 309 const moment = try evt.currentMoment(repo_opts, &repo); 310 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[1], &repo_ids[0] }); 311 312 // the value really was updated 313 const e2r_cur = try moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 314 const e2r = try Repo.DB.HashMap(.read_only).init(e2r_cur); 315 const rc = try e2r.getCursor(hash.hashInt(repo_opts.hash, &repo_ids[0])) orelse return error.NotFound; 316 const rm = try Repo.DB.HashMap(.read_only).init(rc); 317 const re = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, rm); 318 try std.testing.expectEqualStrings("updated", re.event.description); 319 } 320 } 321 322 test "fork query and removal lifecycle" { 323 const io = std.testing.io; 324 const allocator = std.testing.allocator; 325 326 var temp = std.testing.tmpDir(.{}); 327 defer temp.cleanup(); 328 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 329 defer allocator.free(temp_path); 330 331 const repos_dir = try std.fs.path.join(allocator, &.{ temp_path, "repos" }); 332 defer allocator.free(repos_dir); 333 const admin_path = try std.fs.path.join(allocator, &.{ temp_path, "admin" }); 334 defer allocator.free(admin_path); 335 336 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 337 const user_id = evt.EventWithId.randomId(prng.random()); 338 const repo_id = evt.EventWithId.randomId(prng.random()); 339 const repo_id_hex = std.fmt.bytesToHex(repo_id, .lower); 340 const fork_id = evt.EventWithId.randomId(prng.random()); 341 const fork_id_hex = std.fmt.bytesToHex(fork_id, .lower); 342 343 var admin = try rp.Repo(.xit, evt.admin_repo_opts).init(io, allocator, .{ .path = admin_path }); 344 defer admin.deinit(io, allocator); 345 try evt.consume(.server, .admin, .xit, evt.admin_repo_opts, io, allocator, &admin, evt.events_ref, &.{ 346 .{ 347 .id = std.fmt.bytesToHex(user_id, .lower), 348 .timestamp = 1, 349 .author = author, 350 .event = .{ .user = .{ 351 .name = author.name, 352 .email = author.email, 353 .password_hash = "", 354 } }, 355 }, 356 }); 357 const user_repo_path = try evt.userRepoPath(allocator, repos_dir, &user_id); 358 defer allocator.free(user_repo_path); 359 var user_repo = try evt.initUserRepo(io, allocator, user_repo_path); 360 defer user_repo.deinit(io, allocator); 361 try evt.consume(.server, .user, .xit, evt.user_repo_opts, io, allocator, &user_repo, evt.events_ref, &.{ 362 .{ 363 .id = std.fmt.bytesToHex(repo_id, .lower), 364 .timestamp = 1, 365 .author = author, 366 .event = .{ .repo = .{ 367 .user_id = &user_id, 368 .name = "repo", 369 .description = "", 370 } }, 371 }, 372 }); 373 374 const target_path = try std.fs.path.join(allocator, &.{ repos_dir, &repo_id_hex }); 375 defer allocator.free(target_path); 376 var target = try rp.Repo(.xit, fork_repo_opts).init(io, allocator, .{ .path = target_path }); 377 defer target.deinit(io, allocator); 378 { 379 const file = try target.core.work_dir.createFile(io, "README", .{}); 380 defer file.close(io); 381 try file.writeStreamingAll(io, "repo\n"); 382 } 383 try target.add(io, allocator, &.{"README"}); 384 _ = try target.commit(io, allocator, .{ .message = "initial" }); 385 386 const draft_path = try fork.create(fork_repo_opts, io, allocator, repos_dir, &user_repo, .{ 387 .target_branch = "master", 388 .id = fork_id_hex, 389 .user_id = user_id, 390 .repo_id = repo_id, 391 .repo_user_id = user_id, 392 .title = "add a feature", 393 .description = "a draft patch", 394 .labels = "enhancement", 395 .author = author, 396 .timestamp = 2, 397 }); 398 defer allocator.free(draft_path); 399 400 var arena = std.heap.ArenaAllocator.init(allocator); 401 defer arena.deinit(); 402 var moment = try evt.currentMoment(evt.user_repo_opts, &user_repo); 403 var record = (try evt.Fork.readById(evt.UserDB, evt.user_repo_opts.hash, moment, &arena, &fork_id)) orelse return error.NotFound; 404 try std.testing.expectEqualSlices(u8, &repo_id, record.event.repo_id); 405 try std.testing.expectEqualSlices(u8, &user_id, record.event.repo_user_id); 406 try std.testing.expectEqual(.draft, record.event.stage); 407 try std.testing.expectEqual(1, try activeForkCount(moment)); 408 try std.testing.expectEqual(1, try draftForkCount(moment, &repo_id)); 409 410 var draft_repo = try rp.Repo(.xit, fork_repo_opts).open(io, allocator, .{ .path = draft_path }); 411 defer draft_repo.deinit(io, allocator); 412 const draft_moment = try evt.currentMoment(fork_repo_opts, &draft_repo); 413 const draft = (try evt.Patch.readById(evt.EventDB(fork_repo_opts.hash), fork_repo_opts.hash, draft_moment, &arena, &fork_id)) orelse return error.NotFound; 414 try std.testing.expectEqualStrings("add a feature", draft.event.title); 415 try std.testing.expectEqual(null, draft.event.revision); 416 417 try fork.remove(io, allocator, repos_dir, &user_repo, &fork_id_hex, author); 418 try std.testing.expectError(error.FileNotFound, std.Io.Dir.accessAbsolute(io, draft_path, .{})); 419 _ = arena.reset(.retain_capacity); 420 moment = try evt.currentMoment(evt.user_repo_opts, &user_repo); 421 record = (try evt.Fork.readById(evt.UserDB, evt.user_repo_opts.hash, moment, &arena, &fork_id)) orelse return error.NotFound; 422 try std.testing.expect(record.removed); 423 try std.testing.expectEqual(0, try activeForkCount(moment)); 424 try std.testing.expectEqual(0, try draftForkCount(moment, &repo_id)); 425 426 try fork.remove(io, allocator, repos_dir, &user_repo, &fork_id_hex, author); 427 } 428 429 fn draftForkCount(moment: evt.UserDB.HashMap(.read_only), repo_id: []const u8) !u64 { 430 const index_cursor = try moment.getCursor(hash.hashInt(evt.user_repo_opts.hash, evt.Fork.repo_to_draft_id_set_key)) orelse return 0; 431 const index = try evt.UserDB.HashMap(.read_only).init(index_cursor); 432 const ids_cursor = try index.getCursor(hash.hashInt(evt.user_repo_opts.hash, repo_id)) orelse return 0; 433 const ids = try evt.UserDB.SortedSet(.read_only).init(ids_cursor); 434 return try ids.count(); 435 } 436 437 fn activeForkCount(moment: evt.UserDB.HashMap(.read_only)) !u64 { 438 const ids_cursor = try moment.getCursor(hash.hashInt(evt.user_repo_opts.hash, evt.Fork.active_id_set_key)) orelse return 0; 439 const ids = try evt.UserDB.SortedSet(.read_only).init(ids_cursor); 440 return try ids.count(); 441 } 442