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