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