╭ 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 // 36 // define test events 37 // 38 39 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 40 41 const first_event_id = evt.randomId(prng.random()); 42 43 const events_to_consume = [_]evt.Event{ 44 .{ 45 .id = std.fmt.bytesToHex(first_event_id, .lower), 46 .data = .{ 47 .issue = .{ 48 .title = "Login form clears password on validation error", 49 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 50 .tags = "bug\x00priority-high\x00ui", 51 }, 52 }, 53 }, 54 // this event edits the previous one because it has the same id 55 .{ 56 .id = std.fmt.bytesToHex(first_event_id, .lower), 57 .data = .{ 58 .issue = .{ 59 .title = "Login form clears password on validation error", 60 .description = "Submitting an invalid email address resets the password field and removes typed input. Preserve the field value and show an inline validation message.", 61 .tags = "bug\x00priority-low\x00ui", 62 }, 63 }, 64 }, 65 .{ 66 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 67 .data = .{ 68 .issue = .{ 69 .title = "Search results ignore archived project filter", 70 .description = "Filtering search results to active projects still returns issues from archived projects. Apply the archived flag before ranking results.", 71 .tags = "bug\x00search\x00backend", 72 }, 73 }, 74 }, 75 .{ 76 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 77 .data = .{ 78 .issue = .{ 79 .title = "Issue list does not persist selected sort order", 80 .description = "Changing the issue list sort order is lost after refresh. Store the selected sort field and direction with the user's view preferences.", 81 .tags = "enhancement\x00frontend\x00preferences", 82 }, 83 }, 84 }, 85 }; 86 87 // 88 // insert issues as commits in the repo 89 // 90 91 var arena = std.heap.ArenaAllocator.init(allocator); 92 defer arena.deinit(); 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 // rebase the branch so it no longer includes the edit event 160 // 161 162 const rebased_events_to_consume = [_]evt.Event{ 163 events_to_consume[2], 164 events_to_consume[3], 165 }; 166 167 { 168 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 169 defer json.deinit(); 170 171 for (rebased_events_to_consume, 0..) |event, i| { 172 json.clearRetainingCapacity(); 173 174 try std.json.Stringify.value(event, .{}, &json.writer); 175 176 // commit the event into a special branch 177 _ = try repo.commitAtRef( 178 io, 179 allocator, 180 .{ 181 .message = json.written(), 182 // the first event should have the first oid as its parent, 183 // because we want to keep the very first commit. 184 // and the rest will have null which causes them 185 // to have the previous commit as their parent 186 .parent_oids = if (i == 0) &.{std.fmt.bytesToHex(first_oid, .lower)} else null, 187 }, 188 null, 189 .{ .kind = .head, .name = "haxy/meta" }, 190 ); 191 } 192 } 193 194 // 195 // consume events into the database 196 // 197 198 { 199 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 200 201 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 202 203 // read the moment we just created 204 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 205 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 206 207 // get the last object id 208 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 209 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 210 _ = try last_object_id_cursor.readBytes(&last_object_id); 211 212 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 213 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 214 215 try std.testing.expectEqual(2, try haxy.count()); 216 217 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 218 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 219 220 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 221 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 222 223 // get the map of issues 224 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 225 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 226 227 // get the issue out of the map that was edited 228 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_event_id)) orelse return error.NotFound; 229 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 230 const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); 231 232 // the description is no longer edited 233 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.description, first_issue.issue.description); 234 235 // the tags are no longer edited 236 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.tags, first_issue.issue.tags); 237 } 238 239 // 240 // rebase the branch so it no longer includes the original issue 241 // 242 243 { 244 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 245 defer json.deinit(); 246 247 for (rebased_events_to_consume, 0..) |event, i| { 248 json.clearRetainingCapacity(); 249 250 try std.json.Stringify.value(event, .{}, &json.writer); 251 252 // commit the event into a special branch 253 _ = try repo.commitAtRef( 254 io, 255 allocator, 256 .{ 257 .message = json.written(), 258 // the first event should have no parents, 259 // because we want to drop the very first commit. 260 // and the rest will have null which causes them 261 // to have the previous commit as their parent 262 .parent_oids = if (i == 0) &.{} else null, 263 }, 264 null, 265 .{ .kind = .head, .name = "haxy/meta" }, 266 ); 267 } 268 } 269 270 // 271 // consume events into the database 272 // 273 274 { 275 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 276 277 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 278 279 // read the moment we just created 280 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 281 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 282 283 // get the last object id 284 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 285 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 286 _ = try last_object_id_cursor.readBytes(&last_object_id); 287 288 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 289 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 290 291 try std.testing.expectEqual(1, try haxy.count()); 292 293 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 294 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 295 296 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 297 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 298 299 // get the map of issues 300 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 301 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 302 303 // the first issue no longer exists 304 try std.testing.expect(null == try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_event_id))); 305 } 306 } 307 308 test "merge" { 309 const io = std.testing.io; 310 const allocator = std.testing.allocator; 311 const temp_dir_name = "temp-event-merge"; 312 313 // create the temp dir 314 const cwd = std.Io.Dir.cwd(); 315 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 316 if (temp_dir_or_err) |*temp_dir| { 317 temp_dir.close(io); 318 try cwd.deleteTree(io, temp_dir_name); 319 } else |_| {} 320 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 321 defer cwd.deleteTree(io, temp_dir_name) catch {}; 322 defer temp_dir.close(io); 323 324 const cwd_path = try std.process.currentPathAlloc(io, allocator); 325 defer allocator.free(cwd_path); 326 327 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 328 defer allocator.free(work_path); 329 330 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 331 const Repo = rp.Repo(.xit, repo_opts); 332 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 333 defer repo.deinit(io, allocator); 334 335 // 336 // define test events 337 // 338 339 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 340 341 const events_to_consume = [_]evt.Event{ 342 .{ 343 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 344 .data = .{ 345 .issue = .{ 346 .title = "Login form clears password on validation error", 347 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 348 .tags = "bug\x00priority-high\x00ui", 349 }, 350 }, 351 }, 352 .{ 353 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 354 .data = .{ 355 .issue = .{ 356 .title = "Search results ignore archived project filter", 357 .description = "Filtering search results to active projects still returns issues from archived projects. Apply the archived flag before ranking results.", 358 .tags = "bug\x00search\x00backend", 359 }, 360 }, 361 }, 362 .{ 363 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 364 .data = .{ 365 .issue = .{ 366 .title = "Issue list does not persist selected sort order", 367 .description = "Changing the issue list sort order is lost after refresh. Store the selected sort field and direction with the user's view preferences.", 368 .tags = "enhancement\x00frontend\x00preferences", 369 }, 370 }, 371 }, 372 }; 373 374 // 375 // insert issues as commits in the repo 376 // 377 378 var arena = std.heap.ArenaAllocator.init(allocator); 379 defer arena.deinit(); 380 381 var first_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 382 383 { 384 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 385 defer json.deinit(); 386 387 for (events_to_consume, 0..) |event, i| { 388 json.clearRetainingCapacity(); 389 390 try std.json.Stringify.value(event, .{}, &json.writer); 391 392 // commit the event into a special branch 393 const oid = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 394 if (i == 0) { 395 _ = try std.fmt.hexToBytes(&first_oid, &oid); 396 } 397 } 398 } 399 400 // 401 // consume events into the database 402 // 403 404 { 405 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 406 407 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 408 409 // read the moment we just created 410 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 411 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 412 413 // get the last object id 414 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 415 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 416 _ = try last_object_id_cursor.readBytes(&last_object_id); 417 418 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 419 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 420 421 try std.testing.expectEqual(1, try haxy.count()); 422 423 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 424 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 425 426 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 427 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 428 429 // get the map of issues 430 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 431 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 432 433 // get the issue out of the map 434 var first_issue_id: [evt.event_id_size]u8 = undefined; 435 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume[0].id); 436 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; 437 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 438 const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); 439 440 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.description, first_issue.issue.description); 441 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.tags, first_issue.issue.tags); 442 } 443 444 // 445 // define more test events 446 // 447 448 const other_events_to_consume = [_]evt.Event{ 449 .{ 450 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 451 .data = .{ 452 .issue = .{ 453 .title = "Kanban card status badge falls behind after drag", 454 .description = "Moving an issue between columns updates the card position immediately, but the status badge keeps showing the previous state until refresh.", 455 .tags = "bug\x00kanban\x00frontend", 456 }, 457 }, 458 }, 459 .{ 460 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 461 .data = .{ 462 .issue = .{ 463 .title = "Assignee autocomplete omits recently invited users", 464 .description = "Users invited during the current session do not appear in the assignee picker until the project page is reloaded.", 465 .tags = "bug\x00assignees\x00api", 466 }, 467 }, 468 }, 469 .{ 470 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 471 .data = .{ 472 .issue = .{ 473 .title = "Add due date warning for issues blocked by dependencies", 474 .description = "Show a warning when an issue's due date is earlier than an unresolved blocking issue so planners can adjust the schedule.", 475 .tags = "enhancement\x00planning\x00dependencies", 476 }, 477 }, 478 }, 479 }; 480 481 // 482 // insert issues as commits in the repo 483 // 484 485 { 486 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 487 defer json.deinit(); 488 489 for (other_events_to_consume, 0..) |event, i| { 490 json.clearRetainingCapacity(); 491 492 try std.json.Stringify.value(event, .{}, &json.writer); 493 494 // commit the event into a special branch 495 _ = try repo.commitAtRef( 496 io, 497 allocator, 498 // make the first commit a child of the first commit on the haxy/meta branch 499 .{ .parent_oids = if (i == 0) &.{std.fmt.bytesToHex(first_oid, .lower)} else null, .message = json.written() }, 500 null, 501 .{ .kind = .head, .name = "haxy/other" }, 502 ); 503 } 504 } 505 506 // 507 // check out the haxy/meta branch and merge the haxy/other branch into it 508 // 509 510 { 511 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "haxy/meta" } } }); 512 defer result.deinit(); 513 514 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = .{ .kind = .head, .name = "haxy/other" } }} } } }, null); 515 defer merge.deinit(); 516 517 try std.testing.expect(.success == merge.result); 518 } 519 520 // 521 // consume events into the database 522 // 523 524 { 525 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 526 527 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 528 529 // read the moment we just created 530 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 531 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 532 533 // get the last object id 534 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 535 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 536 _ = try last_object_id_cursor.readBytes(&last_object_id); 537 538 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 539 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 540 541 try std.testing.expectEqual(2, try haxy.count()); 542 543 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 544 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 545 546 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 547 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 548 549 // get the map of issues 550 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 551 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 552 553 // make sure one of the original issues is still there 554 { 555 // get the issue out of the map 556 var first_issue_id: [evt.event_id_size]u8 = undefined; 557 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume[0].id); 558 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; 559 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 560 const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); 561 562 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.description, first_issue.issue.description); 563 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.tags, first_issue.issue.tags); 564 } 565 566 // make sure one of the new issues is there 567 { 568 // get the issue out of the map 569 var first_issue_id: [evt.event_id_size]u8 = undefined; 570 _ = try std.fmt.hexToBytes(&first_issue_id, &other_events_to_consume[0].id); 571 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; 572 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 573 const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); 574 575 try std.testing.expectEqualStrings(other_events_to_consume[0].data.issue.description, first_issue.issue.description); 576 try std.testing.expectEqualStrings(other_events_to_consume[0].data.issue.tags, first_issue.issue.tags); 577 } 578 } 579 } 580