╭ event.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const hx = @import("haxy"); 3 const evt = hx.event; 4 const xit = hx.xit; 5 const rp = xit.repo; 6 const hash = xit.hash; 7 8 test "rebase" { 9 const io = std.testing.io; 10 const allocator = std.testing.allocator; 11 const temp_dir_name = "temp-event-rebase"; 12 13 // create the temp dir 14 const cwd = std.Io.Dir.cwd(); 15 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 16 if (temp_dir_or_err) |*temp_dir| { 17 temp_dir.close(io); 18 try cwd.deleteTree(io, temp_dir_name); 19 } else |_| {} 20 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 21 defer cwd.deleteTree(io, temp_dir_name) catch {}; 22 defer temp_dir.close(io); 23 24 const cwd_path = try std.process.currentPathAlloc(io, allocator); 25 defer allocator.free(cwd_path); 26 27 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 28 defer allocator.free(work_path); 29 30 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 31 const Repo = rp.Repo(.xit, repo_opts); 32 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 33 defer repo.deinit(io, allocator); 34 35 var arena = std.heap.ArenaAllocator.init(allocator); 36 defer arena.deinit(); 37 38 // 39 // define test events 40 // 41 42 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 43 44 const first_event_id = evt.randomId(prng.random()); 45 46 const events_to_consume = [_]evt.Event{ 47 .{ 48 .id = std.fmt.bytesToHex(first_event_id, .lower), 49 .data = .{ 50 .issue = .{ 51 .title = "Login form clears password on validation error", 52 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 53 .tags = "bug\x00priority-high\x00ui", 54 }, 55 }, 56 }, 57 // this event edits the previous one because it has the same id 58 .{ 59 .id = std.fmt.bytesToHex(first_event_id, .lower), 60 .data = .{ 61 .issue = .{ 62 .title = "Login form clears password on validation error", 63 .description = "Submitting an invalid email address resets the password field and removes typed input. Preserve the field value and show an inline validation message.", 64 .tags = "bug\x00priority-low\x00ui", 65 }, 66 }, 67 }, 68 .{ 69 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 70 .data = .{ 71 .issue = .{ 72 .title = "Search results ignore archived project filter", 73 .description = "Filtering search results to active projects still returns issues from archived projects. Apply the archived flag before ranking results.", 74 .tags = "bug\x00search\x00backend", 75 }, 76 }, 77 }, 78 .{ 79 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 80 .data = .{ 81 .issue = .{ 82 .title = "Issue list does not persist selected sort order", 83 .description = "Changing the issue list sort order is lost after refresh. Store the selected sort field and direction with the user's view preferences.", 84 .tags = "enhancement\x00frontend\x00preferences", 85 }, 86 }, 87 }, 88 }; 89 90 // 91 // insert issues as commits in the repo 92 // 93 94 var first_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 95 96 { 97 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 98 defer json.deinit(); 99 100 for (events_to_consume, 0..) |event, i| { 101 json.clearRetainingCapacity(); 102 103 try std.json.Stringify.value(event, .{}, &json.writer); 104 105 // commit the event into a special branch 106 const oid = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 107 if (i == 0) { 108 _ = try std.fmt.hexToBytes(&first_oid, &oid); 109 } 110 } 111 } 112 113 // 114 // consume events into the database 115 // 116 117 { 118 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 119 120 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 121 122 // read the moment we just created 123 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 124 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 125 126 // get the last object id 127 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 128 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 129 _ = try last_object_id_cursor.readBytes(&last_object_id); 130 131 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 132 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 133 134 try std.testing.expectEqual(1, try haxy.count()); 135 136 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 137 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 138 139 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 140 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 141 142 // get the map of issues 143 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 144 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 145 146 // get the issue out of the map that was edited 147 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_event_id)) orelse return error.NotFound; 148 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 149 const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); 150 151 // the description was correctly edited 152 try std.testing.expectEqualStrings(events_to_consume[1].data.issue.description, first_issue.issue.description); 153 154 // the tags were correctly edited 155 try std.testing.expectEqualStrings(events_to_consume[1].data.issue.tags, first_issue.issue.tags); 156 } 157 158 // 159 // add another event 160 // 161 162 const events_to_consume2 = [_]evt.Event{ 163 .{ 164 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 165 .data = .{ 166 .issue = .{ 167 .title = "Double clicking causes the form to submit twice", 168 .description = "When I double click the post button I see duplicate posts.", 169 .tags = "bug\x00priority-high\x00ui", 170 }, 171 }, 172 }, 173 }; 174 175 { 176 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 177 defer json.deinit(); 178 179 for (events_to_consume2) |event| { 180 json.clearRetainingCapacity(); 181 182 try std.json.Stringify.value(event, .{}, &json.writer); 183 184 // commit the event into a special branch 185 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 186 } 187 } 188 189 // 190 // consume events into the database 191 // 192 193 { 194 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 195 196 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 197 198 // read the moment we just created 199 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 200 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 201 202 // get the last object id 203 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 204 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 205 _ = try last_object_id_cursor.readBytes(&last_object_id); 206 207 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 208 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 209 210 try std.testing.expectEqual(2, try haxy.count()); 211 } 212 213 // 214 // rebase the branch so it no longer includes the edit event 215 // 216 217 const events_to_consume3 = [_]evt.Event{ 218 events_to_consume[2], 219 events_to_consume[3], 220 }; 221 222 { 223 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 224 defer json.deinit(); 225 226 for (events_to_consume3, 0..) |event, i| { 227 json.clearRetainingCapacity(); 228 229 try std.json.Stringify.value(event, .{}, &json.writer); 230 231 // commit the event into a special branch 232 _ = try repo.commitAtRef( 233 io, 234 allocator, 235 .{ 236 .message = json.written(), 237 // the first event should have the first oid as its parent, 238 // because we want to keep the very first commit. 239 // and the rest will have null which causes them 240 // to have the previous commit as their parent 241 .parent_oids = if (i == 0) &.{std.fmt.bytesToHex(first_oid, .lower)} else null, 242 }, 243 null, 244 .{ .kind = .head, .name = "haxy/meta" }, 245 ); 246 } 247 } 248 249 // 250 // consume events into the database 251 // 252 253 { 254 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 255 256 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 257 258 // read the moment we just created 259 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 260 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 261 262 // get the last object id 263 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 264 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 265 _ = try last_object_id_cursor.readBytes(&last_object_id); 266 267 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 268 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 269 270 try std.testing.expectEqual(2, try haxy.count()); 271 272 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 273 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 274 275 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 276 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 277 278 // get the map of issues 279 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 280 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 281 282 // get the issue out of the map that was edited 283 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_event_id)) orelse return error.NotFound; 284 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 285 const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); 286 287 // the description is no longer edited 288 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.description, first_issue.issue.description); 289 290 // the tags are no longer edited 291 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.tags, first_issue.issue.tags); 292 293 // an event added by the second push is no longer there because it was wiped out by the rebase 294 try std.testing.expect(null == try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &events_to_consume2[0].id))); 295 } 296 297 // 298 // rebase the branch so it no longer includes the original issue 299 // 300 301 { 302 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 303 defer json.deinit(); 304 305 for (events_to_consume3, 0..) |event, i| { 306 json.clearRetainingCapacity(); 307 308 try std.json.Stringify.value(event, .{}, &json.writer); 309 310 // commit the event into a special branch 311 _ = try repo.commitAtRef( 312 io, 313 allocator, 314 .{ 315 .message = json.written(), 316 // the first event should have no parents, 317 // because we want to drop the very first commit. 318 // and the rest will have null which causes them 319 // to have the previous commit as their parent 320 .parent_oids = if (i == 0) &.{} else null, 321 }, 322 null, 323 .{ .kind = .head, .name = "haxy/meta" }, 324 ); 325 } 326 } 327 328 // 329 // consume events into the database 330 // 331 332 { 333 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 334 335 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 336 337 // read the moment we just created 338 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 339 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 340 341 // get the last object id 342 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 343 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 344 _ = try last_object_id_cursor.readBytes(&last_object_id); 345 346 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 347 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 348 349 try std.testing.expectEqual(1, try haxy.count()); 350 351 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 352 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 353 354 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 355 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 356 357 // get the map of issues 358 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 359 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 360 361 // the first issue no longer exists 362 try std.testing.expect(null == try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_event_id))); 363 } 364 } 365 366 test "merge" { 367 const io = std.testing.io; 368 const allocator = std.testing.allocator; 369 const temp_dir_name = "temp-event-merge"; 370 371 // create the temp dir 372 const cwd = std.Io.Dir.cwd(); 373 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 374 if (temp_dir_or_err) |*temp_dir| { 375 temp_dir.close(io); 376 try cwd.deleteTree(io, temp_dir_name); 377 } else |_| {} 378 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 379 defer cwd.deleteTree(io, temp_dir_name) catch {}; 380 defer temp_dir.close(io); 381 382 const cwd_path = try std.process.currentPathAlloc(io, allocator); 383 defer allocator.free(cwd_path); 384 385 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 386 defer allocator.free(work_path); 387 388 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 389 const Repo = rp.Repo(.xit, repo_opts); 390 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 391 defer repo.deinit(io, allocator); 392 393 var arena = std.heap.ArenaAllocator.init(allocator); 394 defer arena.deinit(); 395 396 // 397 // define test events 398 // 399 400 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 401 402 const events_to_consume = [_]evt.Event{ 403 .{ 404 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 405 .data = .{ 406 .issue = .{ 407 .title = "Login form clears password on validation error", 408 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 409 .tags = "bug\x00priority-high\x00ui", 410 }, 411 }, 412 }, 413 .{ 414 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 415 .data = .{ 416 .issue = .{ 417 .title = "Search results ignore archived project filter", 418 .description = "Filtering search results to active projects still returns issues from archived projects. Apply the archived flag before ranking results.", 419 .tags = "bug\x00search\x00backend", 420 }, 421 }, 422 }, 423 .{ 424 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 425 .data = .{ 426 .issue = .{ 427 .title = "Issue list does not persist selected sort order", 428 .description = "Changing the issue list sort order is lost after refresh. Store the selected sort field and direction with the user's view preferences.", 429 .tags = "enhancement\x00frontend\x00preferences", 430 }, 431 }, 432 }, 433 }; 434 435 // 436 // insert issues as commits in the repo 437 // 438 439 var first_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 440 441 { 442 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 443 defer json.deinit(); 444 445 for (events_to_consume, 0..) |event, i| { 446 json.clearRetainingCapacity(); 447 448 try std.json.Stringify.value(event, .{}, &json.writer); 449 450 // commit the event into a special branch 451 const oid = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 452 if (i == 0) { 453 _ = try std.fmt.hexToBytes(&first_oid, &oid); 454 } 455 } 456 } 457 458 // 459 // consume events into the database 460 // 461 462 { 463 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 464 465 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 466 467 // read the moment we just created 468 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 469 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 470 471 // get the last object id 472 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 473 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 474 _ = try last_object_id_cursor.readBytes(&last_object_id); 475 476 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 477 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 478 479 try std.testing.expectEqual(1, try haxy.count()); 480 481 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 482 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 483 484 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 485 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 486 487 // get the map of issues 488 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 489 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 490 491 // get the issue out of the map 492 var first_issue_id: [evt.event_id_size]u8 = undefined; 493 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume[0].id); 494 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; 495 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 496 const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); 497 498 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.description, first_issue.issue.description); 499 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.tags, first_issue.issue.tags); 500 } 501 502 // 503 // define more test events 504 // 505 506 const events_to_consume2 = [_]evt.Event{ 507 .{ 508 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 509 .data = .{ 510 .issue = .{ 511 .title = "Kanban card status badge falls behind after drag", 512 .description = "Moving an issue between columns updates the card position immediately, but the status badge keeps showing the previous state until refresh.", 513 .tags = "bug\x00kanban\x00frontend", 514 }, 515 }, 516 }, 517 .{ 518 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 519 .data = .{ 520 .issue = .{ 521 .title = "Assignee autocomplete omits recently invited users", 522 .description = "Users invited during the current session do not appear in the assignee picker until the project page is reloaded.", 523 .tags = "bug\x00assignees\x00api", 524 }, 525 }, 526 }, 527 .{ 528 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 529 .data = .{ 530 .issue = .{ 531 .title = "Add due date warning for issues blocked by dependencies", 532 .description = "Show a warning when an issue's due date is earlier than an unresolved blocking issue so planners can adjust the schedule.", 533 .tags = "enhancement\x00planning\x00dependencies", 534 }, 535 }, 536 }, 537 }; 538 539 // 540 // insert issues as commits in the repo 541 // 542 543 { 544 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 545 defer json.deinit(); 546 547 for (events_to_consume2, 0..) |event, i| { 548 json.clearRetainingCapacity(); 549 550 try std.json.Stringify.value(event, .{}, &json.writer); 551 552 // commit the event into a special branch 553 _ = try repo.commitAtRef( 554 io, 555 allocator, 556 // make the first commit a child of the first commit on the haxy/meta branch 557 .{ .parent_oids = if (i == 0) &.{std.fmt.bytesToHex(first_oid, .lower)} else null, .message = json.written() }, 558 null, 559 .{ .kind = .head, .name = "haxy/other" }, 560 ); 561 } 562 } 563 564 // 565 // check out the haxy/meta branch and merge the haxy/other branch into it 566 // 567 568 { 569 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "haxy/meta" } } }); 570 defer result.deinit(); 571 572 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = .{ .kind = .head, .name = "haxy/other" } }} } } }, null); 573 defer merge.deinit(); 574 575 try std.testing.expect(.success == merge.result); 576 } 577 578 // 579 // consume events into the database 580 // 581 582 { 583 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 584 585 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 586 587 // read the moment we just created 588 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 589 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 590 591 // get the last object id 592 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 593 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 594 _ = try last_object_id_cursor.readBytes(&last_object_id); 595 596 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 597 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 598 599 try std.testing.expectEqual(2, try haxy.count()); 600 601 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 602 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 603 604 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 605 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 606 607 // get the map of issues 608 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 609 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 610 611 // make sure one of the original issues is still there 612 { 613 // get the issue out of the map 614 var first_issue_id: [evt.event_id_size]u8 = undefined; 615 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume[0].id); 616 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; 617 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 618 const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); 619 620 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.description, first_issue.issue.description); 621 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.tags, first_issue.issue.tags); 622 } 623 624 // make sure one of the new issues is there 625 { 626 // get the issue out of the map 627 var first_issue_id: [evt.event_id_size]u8 = undefined; 628 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume2[0].id); 629 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; 630 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 631 const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); 632 633 try std.testing.expectEqualStrings(events_to_consume2[0].data.issue.description, first_issue.issue.description); 634 try std.testing.expectEqualStrings(events_to_consume2[0].data.issue.tags, first_issue.issue.tags); 635 } 636 } 637 638 // 639 // insert issues as commits in the repo 640 // 641 642 { 643 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 644 defer json.deinit(); 645 646 for (events_to_consume, 0..) |event, i| { 647 json.clearRetainingCapacity(); 648 649 try std.json.Stringify.value(event, .{}, &json.writer); 650 651 // commit the event into a special branch 652 _ = try repo.commitAtRef( 653 io, 654 allocator, 655 // make the first event have no parents so we completely rebase the branch, 656 // undoing the merge that we did above 657 .{ .parent_oids = if (i == 0) &.{} else null, .message = json.written() }, 658 null, 659 .{ .kind = .head, .name = "haxy/meta" }, 660 ); 661 } 662 } 663 664 // 665 // consume events into the database 666 // 667 668 { 669 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 670 671 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 672 673 // read the moment we just created 674 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 675 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 676 677 // get the last object id 678 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 679 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 680 _ = try last_object_id_cursor.readBytes(&last_object_id); 681 682 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 683 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 684 685 try std.testing.expectEqual(1, try haxy.count()); 686 687 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 688 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 689 690 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 691 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 692 693 // get the map of issues 694 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 695 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 696 697 // make sure the new issue is not there 698 { 699 // get the issue out of the map 700 var first_issue_id: [evt.event_id_size]u8 = undefined; 701 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume2[0].id); 702 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)); 703 try std.testing.expect(null == first_issue_cursor); 704 } 705 } 706 } 707