╭ 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 { 95 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 96 defer json.deinit(); 97 98 for (events_to_consume) |event| { 99 json.clearRetainingCapacity(); 100 101 try std.json.Stringify.value(event, .{}, &json.writer); 102 103 // commit the event into a special branch 104 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 105 } 106 } 107 108 // 109 // read and parse all of the events from the repo 110 // 111 112 const events = try readEventsFromRepo(Repo, repo_opts.hash, io, &arena, &repo, "haxy/meta"); 113 114 const first_oid = events[events.len - 1].oid; 115 116 // 117 // consume events into the database 118 // 119 120 { 121 try evt.consume(repo_opts, io, allocator, &repo, events); 122 123 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 124 125 // read the moment we just created 126 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 127 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 128 129 // get the last object id 130 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 131 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 132 _ = try last_object_id_cursor.readBytes(&last_object_id); 133 134 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 135 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 136 137 try std.testing.expectEqual(1, try haxy.count()); 138 139 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 140 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 141 142 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 143 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 144 145 // get the map of issues 146 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 147 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 148 149 // get the issue out of the map that was edited 150 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_event_id)) orelse return error.NotFound; 151 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 152 const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); 153 154 // the description was correctly edited 155 try std.testing.expectEqualStrings(events_to_consume[1].data.issue.description, first_issue.issue.description); 156 157 // the tags were correctly edited 158 try std.testing.expectEqualStrings(events_to_consume[1].data.issue.tags, first_issue.issue.tags); 159 } 160 161 // 162 // rebase the branch so it no longer includes the edit event 163 // 164 165 const rebased_events_to_consume = [_]evt.Event{ 166 events_to_consume[2], 167 events_to_consume[3], 168 }; 169 170 { 171 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 172 defer json.deinit(); 173 174 for (rebased_events_to_consume, 0..) |event, i| { 175 json.clearRetainingCapacity(); 176 177 try std.json.Stringify.value(event, .{}, &json.writer); 178 179 // commit the event into a special branch 180 _ = try repo.commitAtRef( 181 io, 182 allocator, 183 .{ 184 .message = json.written(), 185 // the first event should have the first oid as its parent, 186 // because we want to keep the very first commit. 187 // and the rest will have null which causes them 188 // to have the previous commit as their parent 189 .parent_oids = if (i == 0) &.{std.fmt.bytesToHex(first_oid, .lower)} else null, 190 }, 191 null, 192 .{ .kind = .head, .name = "haxy/meta" }, 193 ); 194 } 195 } 196 197 // 198 // read and parse all of the events from the repo 199 // 200 201 const events2 = try readEventsFromRepo(Repo, repo_opts.hash, io, &arena, &repo, "haxy/meta"); 202 203 // 204 // consume events into the database 205 // 206 207 { 208 try evt.consume(repo_opts, io, allocator, &repo, events2); 209 210 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 211 212 // read the moment we just created 213 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 214 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 215 216 // get the last object id 217 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 218 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 219 _ = try last_object_id_cursor.readBytes(&last_object_id); 220 221 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 222 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 223 224 try std.testing.expectEqual(2, try haxy.count()); 225 226 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 227 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 228 229 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 230 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 231 232 // get the map of issues 233 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 234 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 235 236 // get the issue out of the map that was edited 237 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_event_id)) orelse return error.NotFound; 238 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 239 const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); 240 241 // the description is no longer edited 242 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.description, first_issue.issue.description); 243 244 // the tags are no longer edited 245 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.tags, first_issue.issue.tags); 246 } 247 248 // 249 // rebase the branch so it no longer includes the original issue 250 // 251 252 { 253 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 254 defer json.deinit(); 255 256 for (rebased_events_to_consume, 0..) |event, i| { 257 json.clearRetainingCapacity(); 258 259 try std.json.Stringify.value(event, .{}, &json.writer); 260 261 // commit the event into a special branch 262 _ = try repo.commitAtRef( 263 io, 264 allocator, 265 .{ 266 .message = json.written(), 267 // the first event should have no parents, 268 // because we want to drop the very first commit. 269 // and the rest will have null which causes them 270 // to have the previous commit as their parent 271 .parent_oids = if (i == 0) &.{} else null, 272 }, 273 null, 274 .{ .kind = .head, .name = "haxy/meta" }, 275 ); 276 } 277 } 278 279 // 280 // read and parse all of the events from the repo 281 // 282 283 const events3 = try readEventsFromRepo(Repo, repo_opts.hash, io, &arena, &repo, "haxy/meta"); 284 285 // 286 // consume events into the database 287 // 288 289 { 290 try evt.consume(repo_opts, io, allocator, &repo, events3); 291 292 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 293 294 // read the moment we just created 295 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 296 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 297 298 // get the last object id 299 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 300 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 301 _ = try last_object_id_cursor.readBytes(&last_object_id); 302 303 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 304 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 305 306 try std.testing.expectEqual(1, try haxy.count()); 307 308 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 309 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 310 311 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 312 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 313 314 // get the map of issues 315 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 316 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 317 318 // the first issue no longer exists 319 try std.testing.expect(null == try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_event_id))); 320 } 321 } 322 323 test "merge" { 324 const io = std.testing.io; 325 const allocator = std.testing.allocator; 326 const temp_dir_name = "temp-event-merge"; 327 328 // create the temp dir 329 const cwd = std.Io.Dir.cwd(); 330 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 331 if (temp_dir_or_err) |*temp_dir| { 332 temp_dir.close(io); 333 try cwd.deleteTree(io, temp_dir_name); 334 } else |_| {} 335 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 336 defer cwd.deleteTree(io, temp_dir_name) catch {}; 337 defer temp_dir.close(io); 338 339 const cwd_path = try std.process.currentPathAlloc(io, allocator); 340 defer allocator.free(cwd_path); 341 342 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 343 defer allocator.free(work_path); 344 345 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 346 const Repo = rp.Repo(.xit, repo_opts); 347 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 348 defer repo.deinit(io, allocator); 349 350 // 351 // define test events 352 // 353 354 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 355 356 const events_to_consume = [_]evt.Event{ 357 .{ 358 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 359 .data = .{ 360 .issue = .{ 361 .title = "Login form clears password on validation error", 362 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 363 .tags = "bug\x00priority-high\x00ui", 364 }, 365 }, 366 }, 367 .{ 368 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 369 .data = .{ 370 .issue = .{ 371 .title = "Search results ignore archived project filter", 372 .description = "Filtering search results to active projects still returns issues from archived projects. Apply the archived flag before ranking results.", 373 .tags = "bug\x00search\x00backend", 374 }, 375 }, 376 }, 377 .{ 378 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 379 .data = .{ 380 .issue = .{ 381 .title = "Issue list does not persist selected sort order", 382 .description = "Changing the issue list sort order is lost after refresh. Store the selected sort field and direction with the user's view preferences.", 383 .tags = "enhancement\x00frontend\x00preferences", 384 }, 385 }, 386 }, 387 }; 388 389 // 390 // insert issues as commits in the repo 391 // 392 393 var arena = std.heap.ArenaAllocator.init(allocator); 394 defer arena.deinit(); 395 396 { 397 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 398 defer json.deinit(); 399 400 for (events_to_consume) |event| { 401 json.clearRetainingCapacity(); 402 403 try std.json.Stringify.value(event, .{}, &json.writer); 404 405 // commit the event into a special branch 406 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 407 } 408 } 409 410 // 411 // read and parse all of the events from the repo 412 // 413 414 const events = try readEventsFromRepo(Repo, repo_opts.hash, io, &arena, &repo, "haxy/meta"); 415 416 // 417 // consume events into the database 418 // 419 420 { 421 try evt.consume(repo_opts, io, allocator, &repo, events); 422 423 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 424 425 // read the moment we just created 426 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 427 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 428 429 // get the last object id 430 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 431 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 432 _ = try last_object_id_cursor.readBytes(&last_object_id); 433 434 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 435 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 436 437 try std.testing.expectEqual(1, try haxy.count()); 438 439 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 440 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 441 442 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 443 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 444 445 // get the map of issues 446 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 447 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 448 449 // get the issue out of the map 450 var first_issue_id: [evt.event_id_size]u8 = undefined; 451 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume[0].id); 452 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; 453 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 454 const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); 455 456 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.description, first_issue.issue.description); 457 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.tags, first_issue.issue.tags); 458 } 459 460 // 461 // define more test events 462 // 463 464 const other_events_to_consume = [_]evt.Event{ 465 .{ 466 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 467 .data = .{ 468 .issue = .{ 469 .title = "Kanban card status badge falls behind after drag", 470 .description = "Moving an issue between columns updates the card position immediately, but the status badge keeps showing the previous state until refresh.", 471 .tags = "bug\x00kanban\x00frontend", 472 }, 473 }, 474 }, 475 .{ 476 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 477 .data = .{ 478 .issue = .{ 479 .title = "Assignee autocomplete omits recently invited users", 480 .description = "Users invited during the current session do not appear in the assignee picker until the project page is reloaded.", 481 .tags = "bug\x00assignees\x00api", 482 }, 483 }, 484 }, 485 .{ 486 .id = std.fmt.bytesToHex(evt.randomId(prng.random()), .lower), 487 .data = .{ 488 .issue = .{ 489 .title = "Add due date warning for issues blocked by dependencies", 490 .description = "Show a warning when an issue's due date is earlier than an unresolved blocking issue so planners can adjust the schedule.", 491 .tags = "enhancement\x00planning\x00dependencies", 492 }, 493 }, 494 }, 495 }; 496 497 // 498 // insert issues as commits in the repo 499 // 500 501 { 502 var json: std.Io.Writer.Allocating = .init(std.testing.allocator); 503 defer json.deinit(); 504 505 for (other_events_to_consume, 0..) |event, i| { 506 json.clearRetainingCapacity(); 507 508 try std.json.Stringify.value(event, .{}, &json.writer); 509 510 // commit the event into a special branch 511 _ = try repo.commitAtRef( 512 io, 513 allocator, 514 // make the first commit a child of the first commit on the haxy/meta branch 515 .{ .parent_oids = if (i == 0) &.{std.fmt.bytesToHex(events[events.len - 1].oid, .lower)} else null, .message = json.written() }, 516 null, 517 .{ .kind = .head, .name = "haxy/other" }, 518 ); 519 } 520 } 521 522 // 523 // check out the haxy/meta branch and merge the haxy/other branch into it 524 // 525 526 { 527 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "haxy/meta" } } }); 528 defer result.deinit(); 529 530 const head = (try repo.readRef(io, .{ .kind = .head, .name = "haxy/meta" })).?; 531 try std.testing.expectEqualStrings(&std.fmt.bytesToHex(events[0].oid, .lower), &head); 532 533 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = .{ .kind = .head, .name = "haxy/other" } }} } } }, null); 534 defer merge.deinit(); 535 536 try std.testing.expect(.success == merge.result); 537 } 538 539 // 540 // read and parse all of the events from the repo 541 // 542 543 const events2 = try readEventsFromRepo(Repo, repo_opts.hash, io, &arena, &repo, "haxy/meta"); 544 545 // 546 // consume events into the database 547 // 548 549 { 550 try evt.consume(repo_opts, io, allocator, &repo, events2); 551 552 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 553 554 // read the moment we just created 555 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 556 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 557 558 // get the last object id 559 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 560 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 561 _ = try last_object_id_cursor.readBytes(&last_object_id); 562 563 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 564 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 565 566 try std.testing.expectEqual(2, try haxy.count()); 567 568 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 569 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 570 571 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 572 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 573 574 // get the map of issues 575 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 576 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 577 578 // make sure one of the original issues is still there 579 { 580 // get the issue out of the map 581 var first_issue_id: [evt.event_id_size]u8 = undefined; 582 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume[0].id); 583 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; 584 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 585 const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); 586 587 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.description, first_issue.issue.description); 588 try std.testing.expectEqualStrings(events_to_consume[0].data.issue.tags, first_issue.issue.tags); 589 } 590 591 // make sure one of the new issues is there 592 { 593 // get the issue out of the map 594 var first_issue_id: [evt.event_id_size]u8 = undefined; 595 _ = try std.fmt.hexToBytes(&first_issue_id, &other_events_to_consume[0].id); 596 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; 597 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 598 const first_issue = try evt.EventData.read(Repo.DB, repo_opts.hash, arena.allocator(), first_issue_map, .issue); 599 600 try std.testing.expectEqualStrings(other_events_to_consume[0].data.issue.description, first_issue.issue.description); 601 try std.testing.expectEqualStrings(other_events_to_consume[0].data.issue.tags, first_issue.issue.tags); 602 } 603 } 604 } 605 606 fn readEventsFromRepo( 607 comptime Repo: type, 608 comptime hash_kind: hash.HashKind, 609 io: std.Io, 610 arena: *std.heap.ArenaAllocator, 611 repo: *Repo, 612 branch_name: []const u8, 613 ) ![]evt.RepoEvent(hash_kind) { 614 const allocator = arena.allocator(); 615 616 var events: std.ArrayList(evt.RepoEvent(hash_kind)) = .empty; 617 618 const ref = try repo.readRef(io, .{ .kind = .head, .name = branch_name }) orelse return error.RefNotFound; 619 var commit_iter = try repo.log(io, allocator, &.{ref}); 620 621 // read the message from each commit 622 while (try commit_iter.next()) |commit_object| { 623 var oid: [hash.byteLen(hash_kind)]u8 = undefined; 624 _ = try std.fmt.hexToBytes(&oid, &commit_object.oid); 625 626 const parent_oids = commit_object.content.commit.metadata.parent_oids orelse return error.ParentOidsNotFound; 627 628 if (parent_oids.len > 1) { 629 // this is a merge commit 630 try events.append(allocator, .{ .parent_oid = null, .oid = oid, .event = null }); 631 } else { 632 try commit_object.object_reader.seekTo(commit_object.content.commit.message_position); 633 const message = try commit_object.object_reader.interface.allocRemaining(allocator, .unlimited); 634 const event = try std.json.parseFromSliceLeaky(evt.Event, allocator, message, .{}); 635 636 if (parent_oids.len == 0) { 637 try events.append(allocator, .{ .parent_oid = null, .oid = oid, .event = event }); 638 } else { 639 var parent_oid: [hash.byteLen(hash_kind)]u8 = undefined; 640 _ = try std.fmt.hexToBytes(&parent_oid, &parent_oids[0]); 641 try events.append(allocator, .{ .parent_oid = parent_oid, .oid = oid, .event = event }); 642 } 643 } 644 } 645 646 return events.items; 647 } 648