╭ event.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const hx = @import("haxy"); 3 const evt = hx.event; 4 const xit = hx.xit; 5 const rp = xit.repo; 6 const hash = xit.hash; 7 8 test "rebase" { 9 const io = std.testing.io; 10 const allocator = std.testing.allocator; 11 const temp_dir_name = "temp-event-rebase"; 12 13 // create the temp dir 14 const cwd = std.Io.Dir.cwd(); 15 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 16 if (temp_dir_or_err) |*temp_dir| { 17 temp_dir.close(io); 18 try cwd.deleteTree(io, temp_dir_name); 19 } else |_| {} 20 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 21 defer cwd.deleteTree(io, temp_dir_name) catch {}; 22 defer temp_dir.close(io); 23 24 const cwd_path = try std.process.currentPathAlloc(io, allocator); 25 defer allocator.free(cwd_path); 26 27 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 28 defer allocator.free(work_path); 29 30 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 31 const Repo = rp.Repo(.xit, repo_opts); 32 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 33 defer repo.deinit(io, allocator); 34 35 var arena = std.heap.ArenaAllocator.init(allocator); 36 defer arena.deinit(); 37 38 // 39 // define test events 40 // 41 42 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 43 44 const first_event_id = evt.Event.randomId(prng.random()); 45 46 const events_to_consume = [_]evt.Event{ 47 .{ 48 .id = std.fmt.bytesToHex(first_event_id, .lower), 49 .data = .{ 50 .issue = .{ 51 .title = "Login form clears password on validation error", 52 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 53 .tags = "bug\x00priority-high\x00ui", 54 }, 55 }, 56 }, 57 // this event edits the previous one because it has the same id 58 .{ 59 .id = std.fmt.bytesToHex(first_event_id, .lower), 60 .data = .{ 61 .issue = .{ 62 .title = "Login form clears password on validation error", 63 .description = "Submitting an invalid email address resets the password field and removes typed input. Preserve the field value and show an inline validation message.", 64 .tags = "bug\x00priority-low\x00ui", 65 }, 66 }, 67 }, 68 .{ 69 .id = std.fmt.bytesToHex(evt.Event.randomId(prng.random()), .lower), 70 .data = .{ 71 .issue = .{ 72 .title = "Search results ignore archived project filter", 73 .description = "Filtering search results to active projects still returns issues from archived projects. Apply the archived flag before ranking results.", 74 .tags = "bug\x00search\x00backend", 75 }, 76 }, 77 }, 78 .{ 79 .id = std.fmt.bytesToHex(evt.Event.randomId(prng.random()), .lower), 80 .data = .{ 81 .issue = .{ 82 .title = "Issue list does not persist selected sort order", 83 .description = "Changing the issue list sort order is lost after refresh. Store the selected sort field and direction with the user's view preferences.", 84 .tags = "enhancement\x00frontend\x00preferences", 85 }, 86 }, 87 }, 88 }; 89 90 // 91 // insert issues as commits in the repo 92 // 93 94 var first_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 95 96 { 97 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 98 defer json.deinit(); 99 100 for (events_to_consume, 0..) |event, i| { 101 json.clearRetainingCapacity(); 102 103 try std.json.Stringify.value(event, .{}, &json.writer); 104 105 // commit the event into a special branch 106 const oid = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 107 if (i == 0) { 108 _ = try std.fmt.hexToBytes(&first_oid, &oid); 109 } 110 } 111 } 112 113 // 114 // consume events into the database 115 // 116 117 { 118 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 119 120 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 121 122 // read the moment we just created 123 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 124 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 125 126 // get the last object id 127 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 128 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 129 _ = try last_object_id_cursor.readBytes(&last_object_id); 130 131 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 132 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 133 134 try std.testing.expectEqual(1, try haxy.count()); 135 136 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 137 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 138 139 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 140 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 141 142 // get the map of issues 143 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 144 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 145 146 // get the issue out of the map that was edited 147 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_event_id)) orelse return error.NotFound; 148 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 149 const first_issue = try evt.Issue.read(Repo.DB, repo_opts.hash, &arena, first_issue_map); 150 151 // the description was correctly edited 152 try std.testing.expectEqualStrings(events_to_consume[1].data.issue.?.description, first_issue.description); 153 154 // the tags were correctly edited 155 try std.testing.expectEqualStrings(events_to_consume[1].data.issue.?.tags, first_issue.tags); 156 } 157 158 // 159 // add another event 160 // 161 162 const events_to_consume2 = [_]evt.Event{ 163 .{ 164 .id = std.fmt.bytesToHex(evt.Event.randomId(prng.random()), .lower), 165 .data = .{ 166 .issue = .{ 167 .title = "Double clicking causes the form to submit twice", 168 .description = "When I double click the post button I see duplicate posts.", 169 .tags = "bug\x00priority-high\x00ui", 170 }, 171 }, 172 }, 173 }; 174 175 { 176 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 177 defer json.deinit(); 178 179 for (events_to_consume2) |event| { 180 json.clearRetainingCapacity(); 181 182 try std.json.Stringify.value(event, .{}, &json.writer); 183 184 // commit the event into a special branch 185 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 186 } 187 } 188 189 // 190 // consume events into the database 191 // 192 193 { 194 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 195 196 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 197 198 // read the moment we just created 199 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 200 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 201 202 // get the last object id 203 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 204 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 205 _ = try last_object_id_cursor.readBytes(&last_object_id); 206 207 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 208 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 209 210 try std.testing.expectEqual(2, try haxy.count()); 211 } 212 213 // 214 // rebase the branch so it no longer includes the edit event 215 // 216 217 const events_to_consume3 = [_]evt.Event{ 218 events_to_consume[2], 219 events_to_consume[3], 220 }; 221 222 { 223 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 224 defer json.deinit(); 225 226 for (events_to_consume3, 0..) |event, i| { 227 json.clearRetainingCapacity(); 228 229 try std.json.Stringify.value(event, .{}, &json.writer); 230 231 // commit the event into a special branch 232 _ = try repo.commitAtRef( 233 io, 234 allocator, 235 .{ 236 .message = json.written(), 237 // the first event should have the first oid as its parent, 238 // because we want to keep the very first commit. 239 // and the rest will have null which causes them 240 // to have the previous commit as their parent 241 .parent_oids = if (i == 0) &.{std.fmt.bytesToHex(first_oid, .lower)} else null, 242 }, 243 null, 244 .{ .kind = .head, .name = "haxy/meta" }, 245 ); 246 } 247 } 248 249 // 250 // consume events into the database 251 // 252 253 { 254 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 255 256 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 257 258 // read the moment we just created 259 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 260 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 261 262 // get the last object id 263 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 264 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 265 _ = try last_object_id_cursor.readBytes(&last_object_id); 266 267 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 268 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 269 270 try std.testing.expectEqual(2, try haxy.count()); 271 272 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 273 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 274 275 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 276 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 277 278 // get the map of issues 279 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 280 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 281 282 // get the issue out of the map that was edited 283 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_event_id)) orelse return error.NotFound; 284 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 285 const first_issue = try evt.Issue.read(Repo.DB, repo_opts.hash, &arena, first_issue_map); 286 287 // the description is no longer edited 288 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.?.description, first_issue.description); 289 290 // the tags are no longer edited 291 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.?.tags, first_issue.tags); 292 293 // an event added by the second push is no longer there because it was wiped out by the rebase 294 try std.testing.expect(null == try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &events_to_consume2[0].id))); 295 } 296 297 // 298 // rebase the branch so it no longer includes the original issue 299 // 300 301 { 302 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 303 defer json.deinit(); 304 305 for (events_to_consume3, 0..) |event, i| { 306 json.clearRetainingCapacity(); 307 308 try std.json.Stringify.value(event, .{}, &json.writer); 309 310 // commit the event into a special branch 311 _ = try repo.commitAtRef( 312 io, 313 allocator, 314 .{ 315 .message = json.written(), 316 // the first event should have no parents, 317 // because we want to drop the very first commit. 318 // and the rest will have null which causes them 319 // to have the previous commit as their parent 320 .parent_oids = if (i == 0) &.{} else null, 321 }, 322 null, 323 .{ .kind = .head, .name = "haxy/meta" }, 324 ); 325 } 326 } 327 328 // 329 // consume events into the database 330 // 331 332 { 333 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 334 335 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 336 337 // read the moment we just created 338 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 339 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 340 341 // get the last object id 342 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 343 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 344 _ = try last_object_id_cursor.readBytes(&last_object_id); 345 346 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 347 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 348 349 try std.testing.expectEqual(1, try haxy.count()); 350 351 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 352 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 353 354 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 355 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 356 357 // get the map of issues 358 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 359 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 360 361 // the first issue no longer exists 362 try std.testing.expect(null == try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_event_id))); 363 } 364 } 365 366 test "merge" { 367 const io = std.testing.io; 368 const allocator = std.testing.allocator; 369 const temp_dir_name = "temp-event-merge"; 370 371 // create the temp dir 372 const cwd = std.Io.Dir.cwd(); 373 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 374 if (temp_dir_or_err) |*temp_dir| { 375 temp_dir.close(io); 376 try cwd.deleteTree(io, temp_dir_name); 377 } else |_| {} 378 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 379 defer cwd.deleteTree(io, temp_dir_name) catch {}; 380 defer temp_dir.close(io); 381 382 const cwd_path = try std.process.currentPathAlloc(io, allocator); 383 defer allocator.free(cwd_path); 384 385 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 386 defer allocator.free(work_path); 387 388 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 389 const Repo = rp.Repo(.xit, repo_opts); 390 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 391 defer repo.deinit(io, allocator); 392 393 var arena = std.heap.ArenaAllocator.init(allocator); 394 defer arena.deinit(); 395 396 // 397 // define test events 398 // 399 400 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 401 402 const events_to_consume = [_]evt.Event{ 403 .{ 404 .id = std.fmt.bytesToHex(evt.Event.randomId(prng.random()), .lower), 405 .data = .{ 406 .issue = .{ 407 .title = "Login form clears password on validation error", 408 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 409 .tags = "bug\x00priority-high\x00ui", 410 }, 411 }, 412 }, 413 .{ 414 .id = std.fmt.bytesToHex(evt.Event.randomId(prng.random()), .lower), 415 .data = .{ 416 .issue = .{ 417 .title = "Search results ignore archived project filter", 418 .description = "Filtering search results to active projects still returns issues from archived projects. Apply the archived flag before ranking results.", 419 .tags = "bug\x00search\x00backend", 420 }, 421 }, 422 }, 423 .{ 424 .id = std.fmt.bytesToHex(evt.Event.randomId(prng.random()), .lower), 425 .data = .{ 426 .issue = .{ 427 .title = "Issue list does not persist selected sort order", 428 .description = "Changing the issue list sort order is lost after refresh. Store the selected sort field and direction with the user's view preferences.", 429 .tags = "enhancement\x00frontend\x00preferences", 430 }, 431 }, 432 }, 433 }; 434 435 // 436 // insert issues as commits in the repo 437 // 438 439 var first_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 440 441 { 442 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 443 defer json.deinit(); 444 445 for (events_to_consume, 0..) |event, i| { 446 json.clearRetainingCapacity(); 447 448 try std.json.Stringify.value(event, .{}, &json.writer); 449 450 // commit the event into a special branch 451 const oid = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 452 if (i == 0) { 453 _ = try std.fmt.hexToBytes(&first_oid, &oid); 454 } 455 } 456 } 457 458 // 459 // consume events into the database 460 // 461 462 { 463 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 464 465 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 466 467 // read the moment we just created 468 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 469 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 470 471 // get the last object id 472 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 473 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 474 _ = try last_object_id_cursor.readBytes(&last_object_id); 475 476 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 477 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 478 479 try std.testing.expectEqual(1, try haxy.count()); 480 481 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 482 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 483 484 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 485 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 486 487 // get the map of issues 488 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 489 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 490 491 // get the issue out of the map 492 var first_issue_id: [evt.event_id_size]u8 = undefined; 493 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume[0].id); 494 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; 495 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 496 const first_issue = try evt.Issue.read(Repo.DB, repo_opts.hash, &arena, first_issue_map); 497 498 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.?.description, first_issue.description); 499 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.?.tags, first_issue.tags); 500 } 501 502 // 503 // define more test events 504 // 505 506 const events_to_consume2 = [_]evt.Event{ 507 .{ 508 .id = std.fmt.bytesToHex(evt.Event.randomId(prng.random()), .lower), 509 .data = .{ 510 .issue = .{ 511 .title = "Kanban card status badge falls behind after drag", 512 .description = "Moving an issue between columns updates the card position immediately, but the status badge keeps showing the previous state until refresh.", 513 .tags = "bug\x00kanban\x00frontend", 514 }, 515 }, 516 }, 517 .{ 518 .id = std.fmt.bytesToHex(evt.Event.randomId(prng.random()), .lower), 519 .data = .{ 520 .issue = .{ 521 .title = "Assignee autocomplete omits recently invited users", 522 .description = "Users invited during the current session do not appear in the assignee picker until the project page is reloaded.", 523 .tags = "bug\x00assignees\x00api", 524 }, 525 }, 526 }, 527 .{ 528 .id = std.fmt.bytesToHex(evt.Event.randomId(prng.random()), .lower), 529 .data = .{ 530 .issue = .{ 531 .title = "Add due date warning for issues blocked by dependencies", 532 .description = "Show a warning when an issue's due date is earlier than an unresolved blocking issue so planners can adjust the schedule.", 533 .tags = "enhancement\x00planning\x00dependencies", 534 }, 535 }, 536 }, 537 }; 538 539 // 540 // insert issues as commits in the repo 541 // 542 543 { 544 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 545 defer json.deinit(); 546 547 for (events_to_consume2, 0..) |event, i| { 548 json.clearRetainingCapacity(); 549 550 try std.json.Stringify.value(event, .{}, &json.writer); 551 552 // commit the event into a special branch 553 _ = try repo.commitAtRef( 554 io, 555 allocator, 556 // make the first commit a child of the first commit on the haxy/meta branch 557 .{ .parent_oids = if (i == 0) &.{std.fmt.bytesToHex(first_oid, .lower)} else null, .message = json.written() }, 558 null, 559 .{ .kind = .head, .name = "haxy/other" }, 560 ); 561 } 562 } 563 564 // 565 // check out the haxy/meta branch and merge the haxy/other branch into it 566 // 567 568 const merge_oid = blk: { 569 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "haxy/meta" } } }); 570 defer result.deinit(); 571 572 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = .{ .kind = .head, .name = "haxy/other" } }} } } }, null); 573 defer merge.deinit(); 574 575 try std.testing.expect(.success == merge.result); 576 577 break :blk merge.result.success.oid; 578 }; 579 580 // 581 // consume events into the database 582 // 583 584 { 585 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 586 587 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 588 589 // read the moment we just created 590 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 591 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 592 593 // get the last object id 594 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 595 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 596 _ = try last_object_id_cursor.readBytes(&last_object_id); 597 598 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 599 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 600 601 try std.testing.expectEqual(2, try haxy.count()); 602 603 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 604 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 605 606 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 607 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 608 609 // get the map of issues 610 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 611 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 612 613 // make sure one of the original issues is still there 614 { 615 // get the issue out of the map 616 var first_issue_id: [evt.event_id_size]u8 = undefined; 617 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume[0].id); 618 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; 619 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 620 const first_issue = try evt.Issue.read(Repo.DB, repo_opts.hash, &arena, first_issue_map); 621 622 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.?.description, first_issue.description); 623 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.?.tags, first_issue.tags); 624 } 625 626 // make sure one of the new issues is there 627 { 628 // get the issue out of the map 629 var first_issue_id: [evt.event_id_size]u8 = undefined; 630 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume2[0].id); 631 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; 632 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 633 const first_issue = try evt.Issue.read(Repo.DB, repo_opts.hash, &arena, first_issue_map); 634 635 try std.testing.expectEqualStrings(events_to_consume2[0].data.issue.?.description, first_issue.description); 636 try std.testing.expectEqualStrings(events_to_consume2[0].data.issue.?.tags, first_issue.tags); 637 } 638 } 639 640 // 641 // insert issues as commits in the repo 642 // 643 644 { 645 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 646 defer json.deinit(); 647 648 for (events_to_consume, 0..) |event, i| { 649 json.clearRetainingCapacity(); 650 651 try std.json.Stringify.value(event, .{}, &json.writer); 652 653 // commit the event into a special branch 654 _ = try repo.commitAtRef( 655 io, 656 allocator, 657 // make the first event have no parents so we completely rebase the branch, 658 // undoing the merge that we did above 659 .{ .parent_oids = if (i == 0) &.{} else null, .message = json.written() }, 660 null, 661 .{ .kind = .head, .name = "haxy/meta" }, 662 ); 663 } 664 } 665 666 // 667 // consume events into the database 668 // 669 670 { 671 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 672 673 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 674 675 // read the moment we just created 676 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 677 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 678 679 // get the last object id 680 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 681 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 682 _ = try last_object_id_cursor.readBytes(&last_object_id); 683 684 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 685 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 686 687 try std.testing.expectEqual(1, try haxy.count()); 688 689 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 690 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 691 692 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 693 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 694 695 // get the map of issues 696 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 697 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 698 699 // make sure the new issue is not there 700 { 701 // get the issue out of the map 702 var first_issue_id: [evt.event_id_size]u8 = undefined; 703 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume2[0].id); 704 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)); 705 try std.testing.expect(null == first_issue_cursor); 706 } 707 } 708 709 // 710 // define test events that modify the same issue in conflicting ways 711 // 712 713 const events_to_consume3 = [_]evt.Event{ 714 .{ 715 .id = events_to_consume[0].id, 716 .data = .{ 717 .issue = .{ 718 .title = "Login form clears password on validation error", 719 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 720 .tags = "bug\x00priority-low\x00ui", 721 }, 722 }, 723 }, 724 .{ 725 .id = events_to_consume[0].id, 726 .data = .{ 727 .issue = .{ 728 .title = "Login form clears password on validation error", 729 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 730 .tags = "bug\x00priority-medium\x00ui", 731 }, 732 }, 733 }, 734 }; 735 736 // 737 // insert issues as commits in the repo on different branches 738 // 739 740 { 741 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 742 defer json.deinit(); 743 744 for (events_to_consume3, 0..) |event, i| { 745 json.clearRetainingCapacity(); 746 747 try std.json.Stringify.value(event, .{}, &json.writer); 748 749 // commit the event into a special branch 750 _ = try repo.commitAtRef( 751 io, 752 allocator, 753 .{ .parent_oids = &.{merge_oid}, .message = json.written() }, 754 null, 755 .{ .kind = .head, .name = switch (i) { 756 0 => "haxy/meta", 757 1 => "haxy/other", 758 else => unreachable, 759 } }, 760 ); 761 } 762 } 763 764 // 765 // check out the haxy/meta branch and merge the haxy/other branch into it 766 // 767 768 { 769 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "haxy/meta" } } }); 770 defer result.deinit(); 771 772 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = .{ .kind = .head, .name = "haxy/other" } }} } } }, null); 773 defer merge.deinit(); 774 775 try std.testing.expect(.success == merge.result); 776 } 777 778 // 779 // consume events into the database 780 // 781 782 try std.testing.expectError(error.MergeConflict, evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" })); 783 } 784 785 test "user and repo" { 786 const io = std.testing.io; 787 const allocator = std.testing.allocator; 788 const temp_dir_name = "temp-event-user-and-repo"; 789 790 // create the temp dir 791 const cwd = std.Io.Dir.cwd(); 792 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 793 if (temp_dir_or_err) |*temp_dir| { 794 temp_dir.close(io); 795 try cwd.deleteTree(io, temp_dir_name); 796 } else |_| {} 797 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 798 defer cwd.deleteTree(io, temp_dir_name) catch {}; 799 defer temp_dir.close(io); 800 801 const cwd_path = try std.process.currentPathAlloc(io, allocator); 802 defer allocator.free(cwd_path); 803 804 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 805 defer allocator.free(work_path); 806 807 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 808 const Repo = rp.Repo(.xit, repo_opts); 809 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 810 defer repo.deinit(io, allocator); 811 812 var arena = std.heap.ArenaAllocator.init(allocator); 813 defer arena.deinit(); 814 815 // 816 // define test events 817 // 818 819 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 820 const user_event_id = evt.Event.randomId(prng.random()); 821 const repo_event_id = evt.Event.randomId(prng.random()); 822 823 var first_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 824 const first_password_hash = try evt.User.hashPassword("correct horse battery staple", &first_password_hash_buf, io); 825 826 var second_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 827 const second_password_hash = try evt.User.hashPassword("Tr0ub4dor&3", &second_password_hash_buf, io); 828 829 const events_to_consume = [_]evt.Event{ 830 .{ 831 .id = std.fmt.bytesToHex(user_event_id, .lower), 832 .data = .{ 833 .user = .{ 834 .name = "Alice Example", 835 .email = "alice@example.test", 836 .password_hash = first_password_hash, 837 }, 838 }, 839 }, 840 // this event edits the previous one because it has the same id 841 .{ 842 .id = std.fmt.bytesToHex(user_event_id, .lower), 843 .data = .{ 844 .user = .{ 845 .name = "Alice Example", 846 .email = "alice@example.test", 847 .password_hash = second_password_hash, 848 }, 849 }, 850 }, 851 .{ 852 .id = std.fmt.bytesToHex(repo_event_id, .lower), 853 .data = .{ 854 .repo = .{ 855 .user_id = &user_event_id, 856 .name = "ziglings", 857 .enable_issue = true, 858 }, 859 }, 860 }, 861 }; 862 863 // 864 // insert users and repos as commits in the repo 865 // 866 867 { 868 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 869 defer json.deinit(); 870 871 for (events_to_consume) |event| { 872 json.clearRetainingCapacity(); 873 874 try std.json.Stringify.value(event, .{}, &json.writer); 875 876 // commit the event into a special branch 877 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 878 } 879 } 880 881 // 882 // consume events into the database 883 // 884 885 { 886 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 887 888 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 889 890 // read the moment we just created 891 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 892 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 893 894 // get the last object id 895 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 896 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 897 _ = try last_object_id_cursor.readBytes(&last_object_id); 898 899 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 900 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 901 902 try std.testing.expectEqual(1, try haxy.count()); 903 904 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 905 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 906 907 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 908 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 909 910 // get the map of users 911 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 912 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 913 914 // get the user out of the map that was edited 915 const user_cursor = try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 916 const user_map = try Repo.DB.HashMap(.read_only).init(user_cursor); 917 const user_event = try evt.User.read(Repo.DB, repo_opts.hash, &arena, user_map); 918 919 // the password was correctly edited 920 try std.testing.expectEqualStrings(events_to_consume[1].data.user.?.password_hash, user_event.password_hash); 921 922 // get the map of repos 923 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 924 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 925 926 // get the repo out of the map 927 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 928 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 929 const repo_event = try evt.Repo.read(Repo.DB, repo_opts.hash, &arena, repo_map); 930 931 try std.testing.expectEqualSlices(u8, &user_event_id, repo_event.user_id); 932 try std.testing.expectEqualStrings("ziglings", repo_event.name); 933 934 // get the repos created by the user 935 const user_id_to_repos_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repos")) orelse return error.NotFound; 936 const user_id_to_repos = try Repo.DB.HashMap(.read_only).init(user_id_to_repos_cursor); 937 938 const user_repos_cursor = try user_id_to_repos.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 939 const user_repos = try Repo.DB.CountedHashSet(.read_only).init(user_repos_cursor); 940 941 try std.testing.expectEqual(1, try user_repos.count()); 942 943 try std.testing.expect(null != try user_repos.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id))); 944 } 945 946 // 947 // remove the repo 948 // 949 950 const events_to_consume2 = [_]evt.Event{ 951 .{ 952 .id = std.fmt.bytesToHex(repo_event_id, .lower), 953 .data = .{ .repo = null }, 954 }, 955 }; 956 957 { 958 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 959 defer json.deinit(); 960 961 for (events_to_consume2) |event| { 962 json.clearRetainingCapacity(); 963 964 try std.json.Stringify.value(event, .{}, &json.writer); 965 966 // commit the event into a special branch 967 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 968 } 969 } 970 971 // 972 // consume the removal into the database 973 // 974 975 { 976 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 977 978 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 979 980 // read the moment we just created 981 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 982 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 983 984 // get the last object id 985 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 986 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 987 _ = try last_object_id_cursor.readBytes(&last_object_id); 988 989 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 990 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 991 992 try std.testing.expectEqual(2, try haxy.count()); 993 994 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 995 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 996 997 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 998 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 999 1000 // get the map of repos 1001 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 1002 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 1003 1004 try std.testing.expect(null == try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id))); 1005 1006 // get the repos created by the user 1007 const user_id_to_repos_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repos")) orelse return error.NotFound; 1008 const user_id_to_repos = try Repo.DB.HashMap(.read_only).init(user_id_to_repos_cursor); 1009 1010 const user_repos_cursor = try user_id_to_repos.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 1011 const user_repos = try Repo.DB.CountedHashSet(.read_only).init(user_repos_cursor); 1012 1013 try std.testing.expectEqual(0, try user_repos.count()); 1014 try std.testing.expect(null == try user_repos.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id))); 1015 } 1016 1017 // 1018 // remove the user 1019 // 1020 1021 const events_to_consume3 = [_]evt.Event{ 1022 .{ 1023 .id = std.fmt.bytesToHex(user_event_id, .lower), 1024 .data = .{ .user = null }, 1025 }, 1026 }; 1027 1028 { 1029 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 1030 defer json.deinit(); 1031 1032 for (events_to_consume3) |event| { 1033 json.clearRetainingCapacity(); 1034 1035 try std.json.Stringify.value(event, .{}, &json.writer); 1036 1037 // commit the event into a special branch 1038 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 1039 } 1040 } 1041 1042 // 1043 // consume the user removal into the database 1044 // 1045 1046 { 1047 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 1048 1049 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 1050 1051 // read the moment we just created 1052 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 1053 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 1054 1055 // get the last object id 1056 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 1057 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 1058 _ = try last_object_id_cursor.readBytes(&last_object_id); 1059 1060 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 1061 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 1062 1063 try std.testing.expectEqual(3, try haxy.count()); 1064 1065 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 1066 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 1067 1068 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 1069 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 1070 1071 // get the map of users 1072 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 1073 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 1074 1075 try std.testing.expect(null == try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id))); 1076 } 1077 } 1078